├── .gitattributes ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── Dockerfile ├── LICENSE.txt ├── NuGet.Config ├── README.md ├── appveyor.yml ├── dotnet-commands.sln ├── global.json ├── src ├── dotnet-bar │ ├── commandMetadata.json │ ├── dotnet-bar-a.cmd │ ├── dotnet-bar-a.removeext │ ├── dotnet-bar-b.cmd │ ├── dotnet-bar-b.removeext │ ├── dotnet-bar.xproj │ └── project.json ├── dotnet-baz │ ├── Program.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── commandMetadata.json │ ├── dotnet-baz │ ├── dotnet-baz.cmd │ ├── dotnet-baz.xproj │ └── project.json ├── dotnet-commands │ ├── CommandDirectory.cs │ ├── Dockerfile │ ├── Installer.cs │ ├── LICENSE.txt │ ├── Lister.cs │ ├── Logger.cs │ ├── NugetDownloader.cs │ ├── PackageCommand.cs │ ├── PackageInfo.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Uninstaller.cs │ ├── Updater.cs │ ├── ZipPublish.ps1 │ ├── commandMetadata.json │ ├── docker.ps1 │ ├── dotnet-commands │ ├── dotnet-commands.cmd │ ├── dotnet-commands.xproj │ ├── install.ps1 │ ├── install.sh │ └── project.json └── dotnet-foo │ ├── dotnet-foo.cmd │ ├── dotnet-foo.removeext │ ├── dotnet-foo.xproj │ └── project.json └── test └── IntegrationTests ├── AssemblyInit.cs ├── CommandDirectoryCleanup.cs ├── CommandDirectoryTests.cs ├── DownloadNugetTest.cs ├── DownloadNugetTestWithoutPackageSources.cs ├── GlobalSuppressions.cs ├── InstallerTestForDotNetTool.cs ├── InstallerTestForGenericTool.cs ├── InstallerTestForGenericToolWithSpecificVersion.cs ├── InstallerTestForMultipleCommandsWithMetadata.cs ├── IntegrationTests.xproj ├── ListerListsPackagesInstalled.cs ├── Properties └── AssemblyInfo.cs ├── Retry.cs ├── UninstallerTestForGenericTool.cs ├── UpdaterTestForGenericToolWhenDoesNotNeedUpdate.cs ├── UpdaterTestForGenericToolWhenDoesNotNeedUpdateBecauseGreater.cs ├── UpdaterTestForGenericToolWhenNeedsUpdate.cs └── project.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | 65 | *.removeext text eol=lf 66 | *.sh text eol=lf 67 | dotnet-commands text eol=lf 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Microsoft Azure ApplicationInsights config file 170 | ApplicationInsights.config 171 | 172 | # Windows Store app package directory 173 | AppPackages/ 174 | BundleArtifacts/ 175 | 176 | # Visual Studio cache files 177 | # files ending in .cache can be ignored 178 | *.[Cc]ache 179 | # but keep track of directories ending in .cache 180 | !*.[Cc]ache/ 181 | 182 | # Others 183 | ClientBin/ 184 | [Ss]tyle[Cc]op.* 185 | ~$* 186 | *~ 187 | *.dbmdl 188 | *.dbproj.schemaview 189 | *.pfx 190 | *.publishsettings 191 | node_modules/ 192 | orleans.codegen.cs 193 | 194 | # RIA/Silverlight projects 195 | Generated_Code/ 196 | 197 | # Backup & report files from converting an old project file 198 | # to a newer Visual Studio version. Backup files are not needed, 199 | # because we have git ;-) 200 | _UpgradeReport_Files/ 201 | Backup*/ 202 | UpgradeLog*.XML 203 | UpgradeLog*.htm 204 | 205 | # SQL Server files 206 | *.mdf 207 | *.ldf 208 | 209 | # Business Intelligence projects 210 | *.rdl.data 211 | *.bim.layout 212 | *.bim_*.settings 213 | 214 | # Microsoft Fakes 215 | FakesAssemblies/ 216 | 217 | # GhostDoc plugin setting file 218 | *.GhostDoc.xml 219 | 220 | # Node.js Tools for Visual Studio 221 | .ntvs_analysis.dat 222 | 223 | # Visual Studio 6 build log 224 | *.plg 225 | 226 | # Visual Studio 6 workspace options file 227 | *.opt 228 | 229 | # Visual Studio LightSwitch build output 230 | **/*.HTMLClient/GeneratedArtifacts 231 | **/*.DesktopClient/GeneratedArtifacts 232 | **/*.DesktopClient/ModelManifest.xml 233 | **/*.Server/GeneratedArtifacts 234 | **/*.Server/ModelManifest.xml 235 | _Pvt_Extensions 236 | 237 | # LightSwitch generated files 238 | GeneratedArtifacts/ 239 | ModelManifest.xml 240 | 241 | # Paket dependency manager 242 | .paket/paket.exe 243 | 244 | # FAKE - F# Make 245 | .fake/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | before_install: 5 | - docker pull lambda3/dotnet-commands 6 | script: 7 | - docker run -ti --rm -v "$(pwd):/app" lambda3/dotnet-commands /bin/bash -c 'cd /app && dotnet restore && cd /app/test/IntegrationTests && dotnet test' -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Launch (console)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceRoot}\\bin\\Debug\\netcoreapp1.0\\dotnet-commands.dll", 10 | "args": ["install", "foo"], 11 | "cwd": "${workspaceRoot}", 12 | "externalConsole": false, 13 | "stopAtEntry": false 14 | }, 15 | { 16 | "name": ".NET Core Attach", 17 | "type": "coreclr", 18 | "request": "attach", 19 | "processId": "${command.pickProcess}" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "dotnet", 4 | "isShellCommand": true, 5 | "args": [], 6 | "tasks": [ 7 | { 8 | "taskName": "build", 9 | "args": [ 10 | "${workspaceRoot}\\project.json" 11 | ], 12 | "isBuildCommand": true, 13 | "problemMatcher": "$msCompile" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:1.0.0-preview2-sdk 2 | MAINTAINER Giovanni Bassi 3 | 4 | RUN mkdir /repo & \ 5 | mkdir /app 6 | VOLUME /app 7 | WORKDIR /app 8 | RUN apt-get update && \ 9 | apt-get install -y git curl build-essential vim 10 | RUN git clone https://github.com/Lambda3/dotnet-commands.git /repo && \ 11 | cd /repo && \ 12 | git remote add ssh git@github.com:Lambda3/dotnet-commands.git 13 | 14 | # run with `docker run -ti --name dotnet-commands -v "$(pwd):/app" lambda3/dotnet-commands` -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Giovanni Bassi. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # .NET Commands 2 | 3 | A tool that allows you to use any executable as a .NET CLI Command, with special treatment for .NET Core apps. 4 | 5 | [![Windows Build status](https://img.shields.io/appveyor/ci/Lambda3/dotnet-commands/master.svg?label=windows%20build)](https://ci.appveyor.com/project/lambda3/dotnet-commands) 6 | [![Linux Build status](https://img.shields.io/travis/Lambda3/dotnet-commands/master.svg?label=linux%20build)](https://travis-ci.org/Lambda3/dotnet-commands) 7 | [![Nuget count](https://img.shields.io/nuget/v/dotnet-commands.svg)](https://www.nuget.org/packages/dotnet-commands/) 8 | [![License](https://img.shields.io/badge/licence-Apache%20License%202.0-blue.svg)](https://github.com/Lambda3/dotnet-commands/blob/master/LICENSE.txt) 9 | [![Issues open](https://img.shields.io/github/issues-raw/Lambda3/dotnet-commands.svg)](https://huboard.com/Lambda3/dotnet-commands/) 10 | 11 | Any tool can be a .NET CLI tool, it just has to be in the path, and be named `dotnet-*.` What .NET Commands 12 | does is get the tool from nuget and wire it to the path. It is simple and easy to create tools, and simple 13 | and easy to install, update and uninstall tools. 14 | 15 | This is a community project, free and open source. Everyone is invited to contribute, fork, share and use the code. 16 | 17 | ## Installing 18 | 19 | Use your favorite shell. 20 | 21 | ### Powershell 22 | ````powershell 23 | &{$wc=New-Object System.Net.WebClient;$wc.Proxy=[System.Net.WebRequest]::DefaultWebProxy;$wc.Proxy.Credentials=[System.Net.CredentialCache]::DefaultNetworkCredentials;Invoke-Expression($wc.DownloadString('https://raw.githubusercontent.com/Lambda3/dotnet-commands/master/src/dotnet-commands/install.ps1'))} 24 | ```` 25 | 26 | ### CMD 27 | ````cmd 28 | @powershell -NoProfile -ExecutionPolicy unrestricted -Command "&{$wc=New-Object System.Net.WebClient;$wc.Proxy=[System.Net.WebRequest]::DefaultWebProxy;$wc.Proxy.Credentials=[System.Net.CredentialCache]::DefaultNetworkCredentials;Invoke-Expression($wc.DownloadString('https://raw.githubusercontent.com/Lambda3/dotnet-commands/master/src/dotnet-commands/install.ps1'))}" 29 | ```` 30 | 31 | ### Bash, sh, Fish, etc... 32 | ````bash 33 | \curl -sSL https://raw.githubusercontent.com/Lambda3/dotnet-commands/master/src/dotnet-commands/install.sh | bash 34 | ```` 35 | 36 | ## Running 37 | 38 | Simply run `dotnet commands` to see the options, which are similar to this: 39 | 40 | ```` 41 | .NET Commands 42 | 43 | Usage: 44 | dotnet commands install [@] [--force] [--pre] [--verbose] 45 | dotnet commands uninstall [ --verbose] 46 | dotnet commands update ( | all) [--pre] [--verbose] 47 | dotnet commands (list|ls) [--verbose] 48 | dotnet commands --help 49 | dotnet commands --version 50 | 51 | Options: 52 | --force Installs even if package was already installed. Optional. 53 | --pre Include pre-release versions. Ignored if version is supplied. Optional. 54 | --verbose Verbose. Optional. 55 | --help -h Show this screen. 56 | --version -v Show version. 57 | ```` 58 | 59 | You can try to install `dotnet-foo`, a harmless library to experiment with (the code 60 | is in this repo [here](https://github.com/Lambda3/dotnet-commands/tree/master/src/dotnet-foo)). 61 | 62 | ```powershell 63 | dotnet commands install dotnet-foo 64 | ``` 65 | 66 | And you can then use `dotnet-foo` like this: 67 | 68 | ```powershell 69 | dotnet foo 70 | ``` 71 | 72 | `dotnet-foo` will only work on Windows. 73 | 74 | You can also try `dotnet-bar` (code [here](https://github.com/Lambda3/dotnet-commands/tree/master/src/dotnet-bar)), 75 | which exposes `dotnet bar-aa` and `dotnet bar-bb` commands, and will work on Linux, Mac and Windows. 76 | 77 | ## Writing commands 78 | 79 | It is very simple, either: 80 | 81 | * Create a Nuget which contains an executable (*.exe, *.ps1, or *.cmd) in the `tools` folder named `dotnet-yourtool`; 82 | * Or add a `commandMetadata.json` similar 83 | to [the one in this project](https://github.com/Lambda3/dotnet-commands/blob/master/src/dotnet-commands/commandMetadata.json) 84 | and add it to the `content` folder. 85 | 86 | If we find `project.json` files we will restore them. So, feel free to add any .NET Core Tool, and you don't need to add it's 87 | dependencies to your nupkg, they will be installed when your project is installed, just remember to add them to your `project.json` file. 88 | 89 | If you have a `project.json` file but your tools is not a .NET Core tool, but a full framework (desktop framework, .NET Framework, i.e. .NET 4.6.1) tool 90 | you **will** need to add the dependencies to the your nupkg, as those are stand alone tools, and will not run using 91 | .NET CLI, because they produce binaries which cannot be bootstrapped. 92 | 93 | Non .NET tool work as well, just follow the rules above. 94 | 95 | ## Status 96 | 97 | * We can't yet install a specific version. 98 | 99 | PRs welcome. 100 | 101 | ## Maintainers/Core team 102 | 103 | * [Giovanni Bassi](http://blog.lambda3.com.br/L3/giovannibassi/), aka Giggio, [Lambda3](http://www.lambda3.com.br), [@giovannibassi](https://twitter.com/giovannibassi) 104 | 105 | Contributors can be found at the [contributors](https://github.com/Lambda3/dotnet-commands/graphs/contributors) page on Github. 106 | 107 | ## Contact 108 | 109 | Twitter is the best option. 110 | 111 | ## License 112 | 113 | This software is open source, licensed under the Apache License, Version 2.0. 114 | See [LICENSE.txt](https://github.com/Lambda3/dotnet-commands/blob/master/LICENSE.txt) for details. 115 | Check out the terms of the license before you contribute, fork, copy or do anything 116 | with the code. If you decide to contribute you agree to grant copyright of all your contribution to this project, and agree to 117 | mention clearly if do not agree to these terms. Your work will be licensed with the project at Apache V2, along the rest of the code. 118 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | 3 | clone_folder: c:\projects\dotnet-commands 4 | 5 | shallow_clone: true 6 | 7 | clone_depth: 1 8 | 9 | build_script: 10 | - ps: >- 11 | 12 | dotnet restore 13 | 14 | cd c:\projects\dotnet-commands\src\dotnet-commands 15 | 16 | dotnet build 17 | 18 | cd c:\projects\dotnet-commands\test\IntegrationTests 19 | 20 | dotnet build 21 | 22 | test_script: 23 | - ps: >- 24 | 25 | cd c:\projects\dotnet-commands\test\IntegrationTests 26 | 27 | dotnet test -------------------------------------------------------------------------------- /dotnet-commands.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3ED19195-88EF-418B-9591-73E9E48E96F0}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BE62AA8D-4BFD-4E29-BF30-A5F970D05FB2}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitattributes = .gitattributes 11 | .gitignore = .gitignore 12 | .travis.yml = .travis.yml 13 | appveyor.yml = appveyor.yml 14 | Dockerfile = Dockerfile 15 | global.json = global.json 16 | NuGet.Config = NuGet.Config 17 | README.md = README.md 18 | EndProjectSection 19 | EndProject 20 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "dotnet-commands", "src\dotnet-commands\dotnet-commands.xproj", "{1917C2DA-FEB8-4128-9A22-17DCC9F84AF4}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1B9D8461-1CA0-44CF-960C-F67BBB92DF7D}" 23 | EndProject 24 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "IntegrationTests", "test\IntegrationTests\IntegrationTests.xproj", "{1D1024DB-9159-42DC-9E73-B797AE7F8E4C}" 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8DAA817E-6F6B-4A44-B53F-2A6D0989DB2D}" 27 | EndProject 28 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "dotnet-foo", "src\dotnet-foo\dotnet-foo.xproj", "{474DE765-DD8A-4AA6-8099-ECA92FF38D79}" 29 | EndProject 30 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "dotnet-bar", "src\dotnet-bar\dotnet-bar.xproj", "{4EF24C25-61AE-44A9-9203-1F6B278477A5}" 31 | EndProject 32 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "dotnet-baz", "src\dotnet-baz\dotnet-baz.xproj", "{F6BA3B31-1962-463F-BA15-EA9617BD4489}" 33 | EndProject 34 | Global 35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 36 | Debug|Any CPU = Debug|Any CPU 37 | Release|Any CPU = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {1917C2DA-FEB8-4128-9A22-17DCC9F84AF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {1917C2DA-FEB8-4128-9A22-17DCC9F84AF4}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {1917C2DA-FEB8-4128-9A22-17DCC9F84AF4}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {1917C2DA-FEB8-4128-9A22-17DCC9F84AF4}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {1D1024DB-9159-42DC-9E73-B797AE7F8E4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {1D1024DB-9159-42DC-9E73-B797AE7F8E4C}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {1D1024DB-9159-42DC-9E73-B797AE7F8E4C}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {1D1024DB-9159-42DC-9E73-B797AE7F8E4C}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {474DE765-DD8A-4AA6-8099-ECA92FF38D79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {474DE765-DD8A-4AA6-8099-ECA92FF38D79}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {474DE765-DD8A-4AA6-8099-ECA92FF38D79}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {474DE765-DD8A-4AA6-8099-ECA92FF38D79}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {4EF24C25-61AE-44A9-9203-1F6B278477A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {4EF24C25-61AE-44A9-9203-1F6B278477A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {4EF24C25-61AE-44A9-9203-1F6B278477A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {4EF24C25-61AE-44A9-9203-1F6B278477A5}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {F6BA3B31-1962-463F-BA15-EA9617BD4489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {F6BA3B31-1962-463F-BA15-EA9617BD4489}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {F6BA3B31-1962-463F-BA15-EA9617BD4489}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {F6BA3B31-1962-463F-BA15-EA9617BD4489}.Release|Any CPU.Build.0 = Release|Any CPU 60 | EndGlobalSection 61 | GlobalSection(SolutionProperties) = preSolution 62 | HideSolutionNode = FALSE 63 | EndGlobalSection 64 | GlobalSection(NestedProjects) = preSolution 65 | {1917C2DA-FEB8-4128-9A22-17DCC9F84AF4} = {3ED19195-88EF-418B-9591-73E9E48E96F0} 66 | {1D1024DB-9159-42DC-9E73-B797AE7F8E4C} = {1B9D8461-1CA0-44CF-960C-F67BBB92DF7D} 67 | {474DE765-DD8A-4AA6-8099-ECA92FF38D79} = {8DAA817E-6F6B-4A44-B53F-2A6D0989DB2D} 68 | {4EF24C25-61AE-44A9-9203-1F6B278477A5} = {8DAA817E-6F6B-4A44-B53F-2A6D0989DB2D} 69 | {F6BA3B31-1962-463F-BA15-EA9617BD4489} = {8DAA817E-6F6B-4A44-B53F-2A6D0989DB2D} 70 | EndGlobalSection 71 | EndGlobal 72 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src", "test" ], 3 | "sdk": { 4 | "version": "1.0.0-preview2-003121" 5 | } 6 | } -------------------------------------------------------------------------------- /src/dotnet-bar/commandMetadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": { 3 | "dotnet-bar-aa": "tools/dotnet-bar-a.cmd", 4 | "dotnet-bar-bb": "tools/dotnet-bar-b.cmd" 5 | } 6 | } -------------------------------------------------------------------------------- /src/dotnet-bar/dotnet-bar-a.cmd: -------------------------------------------------------------------------------- 1 | @echo Hello from Bar A -------------------------------------------------------------------------------- /src/dotnet-bar/dotnet-bar-a.removeext: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo Hello from Bar A -------------------------------------------------------------------------------- /src/dotnet-bar/dotnet-bar-b.cmd: -------------------------------------------------------------------------------- 1 | @echo Hello from Bar B -------------------------------------------------------------------------------- /src/dotnet-bar/dotnet-bar-b.removeext: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo Hello from Bar B -------------------------------------------------------------------------------- /src/dotnet-bar/dotnet-bar.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0.25420 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 4ef24c25-61ae-44a9-9203-1f6b278477a5 10 | dotnet-bar 11 | .\obj 12 | .\bin\ 13 | 14 | 15 | 16 | 2.0 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/dotnet-bar/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.2-*", 3 | "description": "A sample package only to validate that the tools installer works.", 4 | "dependencies": { 5 | }, 6 | "authors": [ "giggio" ], 7 | "packOptions": { 8 | "owners": [ 9 | "giggio" 10 | ], 11 | "files": { 12 | "mappings": { 13 | "content/commandMetadata.json": "commandMetadata.json", 14 | "tools/dotnet-bar-a.cmd": "dotnet-bar-a.cmd", 15 | "tools/dotnet-bar-b.cmd": "dotnet-bar-b.cmd", 16 | "tools/dotnet-bar-a.removeext": "dotnet-bar-a.removeext", 17 | "tools/dotnet-bar-b.removeext": "dotnet-bar-b.removeext" 18 | } 19 | } 20 | }, 21 | "frameworks": { 22 | "netcoreapp1.0": {} 23 | } 24 | } -------------------------------------------------------------------------------- /src/dotnet-baz/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace dotnet_baz 3 | { 4 | public class Program 5 | { 6 | public static void Main() => 7 | Console.WriteLine("Hello from .NET Commands!"); 8 | } 9 | } -------------------------------------------------------------------------------- /src/dotnet-baz/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("dotnet_baz")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("f6ba3b31-1962-463f-ba15-ea9617bd4489")] 20 | -------------------------------------------------------------------------------- /src/dotnet-baz/commandMetadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "lib/netcoreapp1.0/dotnet-baz.cmd" 3 | } -------------------------------------------------------------------------------- /src/dotnet-baz/dotnet-baz: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dotnet $DIR/dotnet-baz.dll "$@" -------------------------------------------------------------------------------- /src/dotnet-baz/dotnet-baz.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | dotnet %~dp0\dotnet-baz.dll %* -------------------------------------------------------------------------------- /src/dotnet-baz/dotnet-baz.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | f6ba3b31-1962-463f-ba15-ea9617bd4489 11 | dotnet_baz 12 | .\obj 13 | .\bin\ 14 | v4.6.1 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/dotnet-baz/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "dependencies": { 4 | "Microsoft.NETCore.App": { 5 | "type": "platform", 6 | "version": "1.0.0" 7 | } 8 | }, 9 | "frameworks": { 10 | "netcoreapp1.0": { } 11 | }, 12 | "packOptions": { 13 | "owners": [ "giggio" ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/Lambda3/dotnet-commands" 17 | }, 18 | "files": { 19 | "mappings": { 20 | "content/commandMetadata.json": "commandMetadata.json", 21 | "lib/netcoreapp1.0/dotnet-baz.cmd": "dotnet-baz.cmd", 22 | "lib/netcoreapp1.0/dotnet-baz": "dotnet-baz", 23 | "lib/netcoreapp1.0/project.json": "project.json", 24 | "lib/netcoreapp1.0/project.lock.json": "project.lock.json", 25 | "lib/netcoreapp1.0/dotnet-baz.deps.json": "bin/**/netcoreapp1.0/dotnet-baz.deps.json" 26 | } 27 | } 28 | }, 29 | "publishOptions": { 30 | "includeFiles": [ 31 | "dotnet-baz.cmd", 32 | "dotnet-baz" 33 | ] 34 | }, 35 | "buildOptions": { 36 | "debugType": "portable", 37 | "emitEntryPoint": true, 38 | "copyToOutput": { 39 | "includeFiles": [ 40 | "dotnet-baz.cmd", 41 | "dotnet-baz" 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/dotnet-commands/CommandDirectory.cs: -------------------------------------------------------------------------------- 1 | using NuGet.Versioning; 2 | using System.IO; 3 | 4 | namespace DotNetCommands 5 | { 6 | public class CommandDirectory 7 | { 8 | public string BaseDir { get; private set; } 9 | public string PackagesDir { get; private set; } 10 | private readonly string binDir; 11 | public CommandDirectory(string baseDir) 12 | { 13 | if (string.IsNullOrWhiteSpace(baseDir)) throw new System.ArgumentException("You have to supply the base dir.", nameof(baseDir)); 14 | BaseDir = !baseDir.EndsWith(Path.DirectorySeparatorChar.ToString()) 15 | ? baseDir + Path.DirectorySeparatorChar 16 | : baseDir; 17 | PackagesDir = Path.Combine(baseDir, "packages"); 18 | binDir = Path.Combine(baseDir, "bin"); 19 | if (!Directory.Exists(baseDir)) 20 | Directory.CreateDirectory(baseDir); 21 | if (!Directory.Exists(PackagesDir)) 22 | Directory.CreateDirectory(PackagesDir); 23 | if (!Directory.Exists(binDir)) 24 | Directory.CreateDirectory(binDir); 25 | } 26 | 27 | public string GetDirectoryForPackage(string packageName, SemanticVersion packageVersion = null) => 28 | packageVersion == null 29 | ? Path.Combine(PackagesDir, packageName) 30 | : Path.Combine(PackagesDir, packageName, packageVersion.ToString()); 31 | 32 | public string GetBinFile(string fileName) => 33 | Path.Combine(binDir, fileName.Replace('/', Path.DirectorySeparatorChar)); 34 | 35 | public string MakeRelativeToBaseDir(string destination) 36 | { 37 | if (!destination.StartsWith(BaseDir)) 38 | throw new System.ArgumentException(nameof(destination), $"Destination file '{destination}' should start with '{BaseDir}'."); 39 | return ".." + Path.DirectorySeparatorChar + destination.Substring(BaseDir.Length); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/dotnet-commands/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:1.0.0-preview2-sdk 2 | MAINTAINER giggio@giggio.net 3 | 4 | RUN apt-get update 5 | RUN apt-get install git vim -y 6 | 7 | VOLUME [ "/app" ] 8 | WORKDIR /app 9 | 10 | ENTRYPOINT [ "/bin/bash" ] -------------------------------------------------------------------------------- /src/dotnet-commands/Installer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Newtonsoft.Json; 3 | using static DotNetCommands.Logger; 4 | using System.IO; 5 | using System; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using System.Runtime.InteropServices; 9 | using NuGet.Versioning; 10 | 11 | namespace DotNetCommands 12 | { 13 | public class Installer 14 | { 15 | private readonly CommandDirectory commandDirectory; 16 | public Installer(CommandDirectory commandDirectory) 17 | { 18 | this.commandDirectory = commandDirectory; 19 | } 20 | 21 | public async Task InstallAsync(string packageName, SemanticVersion packageVersion, bool force, bool includePreRelease) 22 | { 23 | WriteLineIfVerbose($"Installing {packageName}..."); 24 | PackageInfo packageInfo; 25 | try 26 | { 27 | using (var downloader = new NugetDownloader(commandDirectory)) 28 | { 29 | packageInfo = await downloader.DownloadAndExtractNugetAsync(packageName, packageVersion, force, includePreRelease); 30 | if (packageInfo == null) return false; 31 | } 32 | } 33 | catch (Exception ex) 34 | { 35 | WriteLineIfVerbose("Could not download Nuget."); 36 | WriteLineIfVerbose(ex.ToString()); 37 | return false; 38 | } 39 | var created = CreateBinFile(packageInfo); 40 | if (!created) 41 | { 42 | if (Directory.Exists(packageInfo.PackageDir)) 43 | { 44 | WriteLineIfVerbose($"Deleting {packageInfo}..."); 45 | Directory.Delete(packageInfo.PackageDir, true); 46 | var packageDirectoryForAllVersions = commandDirectory.GetDirectoryForPackage(packageName); 47 | if (Directory.EnumerateDirectories(packageDirectoryForAllVersions).Count() <= 1) 48 | Directory.Delete(packageDirectoryForAllVersions, true); 49 | } 50 | return false; 51 | } 52 | var added = CreateRuntimeConfigDevJsonFile(packageInfo.PackageDir); 53 | if (!added) return false; 54 | var restored = await RestoreAsync(packageInfo.PackageDir); 55 | return restored; 56 | } 57 | 58 | 59 | private static bool CreateRuntimeConfigDevJsonFile(string packageDir) 60 | { 61 | var libDir = Path.Combine(packageDir, "lib"); 62 | if (!Directory.Exists(libDir)) return true; 63 | var commandDlls = Directory.EnumerateFiles(libDir, "*.dll", SearchOption.AllDirectories); 64 | foreach (var commandDll in commandDlls) 65 | { 66 | var command = Path.GetFileNameWithoutExtension(commandDll); 67 | var runtimeConfigDevJsonFile = $"{command}.runtimeconfig.dev.json"; 68 | var runtimeConfigDevJsonFullPath = Path.Combine(Path.GetDirectoryName(commandDll), runtimeConfigDevJsonFile); 69 | if (File.Exists(runtimeConfigDevJsonFullPath)) 70 | { 71 | WriteLineIfVerbose($"File '{runtimeConfigDevJsonFullPath}' already exists, not creating."); 72 | return true; 73 | } 74 | var runtimeConfigJsonFile = $"{command}.runtimeconfig.json"; 75 | var runtimeConfigJsonFullPath = Path.Combine(Path.GetDirectoryName(commandDll), runtimeConfigJsonFile); 76 | if (!File.Exists(runtimeConfigJsonFullPath)) 77 | continue; 78 | var homeDir = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("userprofile"); 79 | if (string.IsNullOrWhiteSpace(homeDir)) 80 | { 81 | WriteLineIfVerbose("Could not find home dir."); 82 | return false; 83 | } 84 | WriteLineIfVerbose($"Creating '{runtimeConfigDevJsonFile}'"); 85 | var packagesDir = Path.Combine(homeDir, ".nuget", "packages"); 86 | var escapedPackagesDir = JsonConvert.SerializeObject(packagesDir); 87 | escapedPackagesDir = escapedPackagesDir.Substring(1, escapedPackagesDir.Length - 2); 88 | File.WriteAllText(runtimeConfigDevJsonFullPath, @"{ 89 | ""runtimeOptions"": { 90 | ""additionalProbingPaths"": [ """ + escapedPackagesDir + @""" ] 91 | } 92 | }"); 93 | WriteLineIfVerbose($"Wrote '{runtimeConfigDevJsonFullPath}' for correct probing paths."); 94 | } 95 | return true; 96 | } 97 | 98 | private async static Task RestoreAsync(string packageDir) 99 | { 100 | var libDir = Path.Combine(packageDir, "lib"); 101 | if (!Directory.Exists(libDir)) return true; 102 | var projectJsons = Directory.EnumerateFiles(libDir, "project.json", SearchOption.AllDirectories); 103 | foreach (var projectJson in projectJsons) 104 | { 105 | WriteLineIfVerbose($"Restoring '{projectJson}'"); 106 | var startInfo = new ProcessStartInfo 107 | { 108 | UseShellExecute = false, 109 | RedirectStandardOutput = true, 110 | RedirectStandardError = true, 111 | CreateNoWindow = true, 112 | FileName = "dotnet", 113 | Arguments = "restore", 114 | WorkingDirectory = Path.GetDirectoryName(projectJson) 115 | }; 116 | using (var process = new Process { StartInfo = startInfo }) 117 | { 118 | process.Start(); 119 | var textResult = await process.StandardOutput.ReadToEndAsync(); 120 | WriteLine(textResult); 121 | process.WaitForExit(); 122 | if (process.ExitCode != 0) return false; 123 | } 124 | } 125 | return true; 126 | } 127 | 128 | private bool CreateBinFile(PackageInfo packageInfo) 129 | { 130 | if (packageInfo == null || !packageInfo.Commands.Any()) return false; 131 | foreach (var command in packageInfo.Commands) 132 | { 133 | var binFile = commandDirectory.GetBinFile(command.Name + (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")); 134 | var relativeMainFileName = commandDirectory.MakeRelativeToBaseDir(command.ExecutableFilePath); 135 | File.WriteAllText(binFile, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 136 | ? $@"@""%~dp0\{relativeMainFileName}"" %*" 137 | : $"#!/usr/bin/env bash{Environment.NewLine}. \"$( cd \"$( dirname \"${{BASH_SOURCE[0]}}\" )\" && pwd )/{relativeMainFileName}\" \"$@\""); 138 | WriteLineIfVerbose($"Wrote redirect file '{binFile}' pointing to '{relativeMainFileName}'."); 139 | } 140 | return true; 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/dotnet-commands/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Giovanni Bassi and Elemar Jr. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/dotnet-commands/Lister.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using static DotNetCommands.Logger; 7 | 8 | namespace DotNetCommands 9 | { 10 | public class Lister 11 | { 12 | private CommandDirectory commandDirectory; 13 | 14 | public Lister(CommandDirectory commandDirectory) 15 | { 16 | this.commandDirectory = commandDirectory; 17 | } 18 | 19 | public async Task ListAsync() 20 | { 21 | var sb = new StringBuilder(); 22 | try 23 | { 24 | var packageDirectories = Directory.EnumerateDirectories(commandDirectory.PackagesDir).OrderBy(d => d); 25 | foreach (var pkgDir in packageDirectories) 26 | { 27 | var packageDirectory = pkgDir; 28 | if (!packageDirectory.EndsWith(Path.DirectorySeparatorChar.ToString())) 29 | packageDirectory += Path.DirectorySeparatorChar.ToString(); 30 | var packageName = Path.GetFileName(Path.GetDirectoryName(packageDirectory)); 31 | var versionDirectories = Directory.EnumerateDirectories(packageDirectory).OrderBy(d => d); 32 | foreach (var verDir in versionDirectories) 33 | { 34 | var versionDirectory = verDir; 35 | if (!versionDirectory.EndsWith(Path.DirectorySeparatorChar.ToString())) 36 | versionDirectory += Path.DirectorySeparatorChar.ToString(); 37 | var packageVersion = Path.GetFileName(Path.GetDirectoryName(versionDirectory)); 38 | sb.AppendLine($"{packageName} ({packageVersion})"); 39 | } 40 | 41 | var packageDirs = Directory.EnumerateDirectories(commandDirectory.GetDirectoryForPackage(packageName)).OrderBy(d => d); 42 | foreach (var packageDir in packageDirs) 43 | { 44 | var packageInfo = await PackageInfo.GetMainFilePathAsync(packageName, packageDir); 45 | if (packageInfo == null || !packageInfo.Commands.Any()) continue; 46 | foreach (var command in packageInfo.Commands) 47 | { 48 | if (command.Name == packageName) continue; 49 | if (IsVerbose) 50 | sb.AppendLine($" {command.Name} ({command.ExecutableFilePath})"); 51 | else 52 | sb.AppendLine($" {command.Name}"); 53 | } 54 | } 55 | } 56 | var packagesInfo = sb.ToString(); 57 | WriteLine(packagesInfo); 58 | return true; 59 | } 60 | catch (Exception ex) 61 | { 62 | WriteLineIfVerbose($"Error getting package info: '{ex.Message}'."); 63 | return false; 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/dotnet-commands/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetCommands 4 | { 5 | public static class Logger 6 | { 7 | private static Action logger = msg => Console.WriteLine(msg); 8 | public static bool IsVerbose = true; 9 | 10 | public static void SetLogger(Action logger) 11 | { 12 | if (logger == null) throw new ArgumentException("Logger can't be null.", nameof(logger)); 13 | Logger.logger = logger; 14 | } 15 | 16 | public static void WriteLineIfVerbose(string msg) 17 | { 18 | if (IsVerbose) 19 | logger(msg); 20 | } 21 | public static void WriteLine(string msg) => logger(msg); 22 | } 23 | } -------------------------------------------------------------------------------- /src/dotnet-commands/NugetDownloader.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NuGet.Common; 3 | using NuGet.Configuration; 4 | using NuGet.Versioning; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Net.Http; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using static DotNetCommands.Logger; 13 | 14 | namespace DotNetCommands 15 | { 16 | public sealed class NugetDownloader : IDisposable 17 | { 18 | private readonly CommandDirectory commandDirectory; 19 | private readonly IList sourceInfos; 20 | 21 | public NugetDownloader(CommandDirectory commandDirectory) 22 | { 23 | this.commandDirectory = commandDirectory; 24 | var sources = GetSources(); 25 | sourceInfos = sources.Select(s => new SourceInfo(s)).ToList(); 26 | } 27 | 28 | private class PackageSourceComparer : IEqualityComparer 29 | { 30 | public bool Equals(PackageSource x, PackageSource y) => string.Compare(x.Source, y.Source, true) == 0; 31 | public int GetHashCode(PackageSource source) => source.Source.GetHashCode() * 13; 32 | } 33 | 34 | private static readonly PackageSourceComparer packageSourceComparer = new PackageSourceComparer(); 35 | 36 | private static IList GetSources() 37 | { 38 | var settings = Settings.LoadDefaultSettings(Directory.GetCurrentDirectory(), configFileName: null, machineWideSettings: new MachineWideSettings()); 39 | var sourceProvider = new PackageSourceProvider(settings); 40 | var sources = sourceProvider.LoadPackageSources().Where(s => s.ProtocolVersion == 3 || s.Source.EndsWith(@"/v3/index.json")).Distinct(packageSourceComparer).ToList(); 41 | if (!sources.Any()) 42 | { 43 | var source = new PackageSource("https://api.nuget.org/v3/index.json", "api.nuget.org", isEnabled: true, isOfficial: true, isPersistable: true) 44 | { 45 | ProtocolVersion = 3 46 | }; 47 | sources.Add(source); 48 | } 49 | return sources; 50 | } 51 | 52 | public async Task GetLatestVersionAsync(string packageName, bool includePreRelease) 53 | { 54 | if (!sourceInfos.Any()) 55 | { 56 | WriteLine("No NuGet sources found."); 57 | return null; 58 | } 59 | return (await GetLatestVersionAndSourceInfoAsync(packageName, includePreRelease))?.Version.ToString(); 60 | } 61 | 62 | private async Task GetSpecificVersionAndSourceInfoAsync(string packageName, SemanticVersion packageVersion) 63 | { 64 | foreach (var sourceInfo in sourceInfos) 65 | { 66 | WriteLineIfVerbose($"Searching for '{packageName}@{packageVersion.ToString()}' on '{sourceInfo.Source.Name}'..."); 67 | NugetVersion latestNugetVersion; 68 | try 69 | { 70 | latestNugetVersion = await sourceInfo.GetPackageAsync(packageName, packageVersion, true); 71 | } 72 | catch (Exception ex) 73 | { 74 | WriteLine($"Error when getting '{packageName}@{packageVersion.ToString()}'. Source used: '{sourceInfo.Source.Name}'."); 75 | WriteLineIfVerbose($"Error details: {ex.ToString()}'."); 76 | return null; 77 | } 78 | if (latestNugetVersion == null) 79 | { 80 | WriteLineIfVerbose($"Could not get a version for '{packageName}@{packageVersion.ToString()}' on '{sourceInfo.Source.Name}'."); 81 | continue; 82 | } 83 | else 84 | { 85 | WriteLineIfVerbose($"Found version '{latestNugetVersion.Version}' for '{packageName}@{packageVersion.ToString()}' on '{sourceInfo.Source.Name}'."); 86 | return latestNugetVersion; 87 | } 88 | } 89 | WriteLine($"Package '{packageName}' not found. Sources used:"); 90 | foreach (var source in sourceInfos.Select(p => p.Source)) 91 | WriteLine($" - {source.Name}: {source.Source}"); 92 | return null; 93 | } 94 | 95 | private async Task GetLatestVersionAndSourceInfoAsync(string packageName, bool includePreRelease) 96 | { 97 | NugetVersion currentNugetVersion = null; 98 | foreach (var sourceInfo in sourceInfos) 99 | { 100 | WriteLineIfVerbose($"Searching for '{packageName}' on '{sourceInfo.Source.Name}'..."); 101 | NugetVersion latestNugetVersion; 102 | try 103 | { 104 | latestNugetVersion = await sourceInfo.GetPackageAsync(packageName, null, includePreRelease); 105 | } 106 | catch (Exception ex) 107 | { 108 | WriteLine($"Error when getting '{packageName}'. Source used: '{sourceInfo.Source.Name}'."); 109 | WriteLineIfVerbose($"Error details: {ex.ToString()}'."); 110 | return null; 111 | } 112 | if (latestNugetVersion == null) 113 | { 114 | WriteLineIfVerbose($"Could not get a version for '{packageName}' on '{sourceInfo.Source.Name}'."); 115 | continue; 116 | } 117 | else 118 | { 119 | WriteLineIfVerbose($"Found version '{latestNugetVersion.Version}' for '{packageName}' on '{sourceInfo.Source.Name}'."); 120 | } 121 | if (currentNugetVersion == null || latestNugetVersion.Version > currentNugetVersion.Version) 122 | currentNugetVersion = latestNugetVersion; 123 | } 124 | if (currentNugetVersion == null) 125 | { 126 | WriteLine($"Package '{packageName}' not found. Sources used:"); 127 | foreach (var source in sourceInfos.Select(p => p.Source)) 128 | WriteLine($" - {source.Name}: {source.Source}"); 129 | return null; 130 | } 131 | WriteLineIfVerbose($"Latest version for '{packageName}' is '{currentNugetVersion.Version}' from '{currentNugetVersion.SourceInfo.Source.Name}'."); 132 | return currentNugetVersion; 133 | } 134 | 135 | /// 136 | /// Downloads the specified nupkg and extracts it to a directory 137 | /// 138 | /// The command to download, i.e. "dotnet-foo". 139 | /// The version to install. If null, then latest will be used. 140 | /// Force the download be made again if it was already downloaded earlier. 141 | /// Allow pre-release versions. 142 | /// The directory where it is extracted 143 | public async Task DownloadAndExtractNugetAsync(string packageName, SemanticVersion packageVersion, bool force, bool includePreRelease) 144 | { 145 | if (!sourceInfos.Any()) 146 | { 147 | WriteLine("No NuGet sources found."); 148 | return null; 149 | } 150 | var nugetVersion = packageVersion == null 151 | ? await GetLatestVersionAndSourceInfoAsync(packageName, includePreRelease) 152 | : await GetSpecificVersionAndSourceInfoAsync(packageName, packageVersion); 153 | if (nugetVersion == null) 154 | { 155 | WriteLineIfVerbose($"Could not get latest version for package '{packageName}'."); 156 | return null; 157 | } 158 | var nupkgResponse = await nugetVersion.GetNupkgAsync(nugetVersion.PackageName); 159 | if (nupkgResponse == null) 160 | { 161 | WriteLineIfVerbose($"Could not get a valid response for the nupkg download."); 162 | return null; 163 | } 164 | if (!nupkgResponse.IsSuccessStatusCode) 165 | { 166 | WriteLineIfVerbose($"Did not get a successful status code. Got {(int)nupkgResponse.StatusCode} ({nupkgResponse.StatusCode})."); 167 | return null; 168 | } 169 | var tempFilePath = Path.GetTempFileName(); 170 | WriteLineIfVerbose($"Saving to '{tempFilePath}'."); 171 | using (var tempFileStream = File.OpenWrite(tempFilePath)) 172 | await nupkgResponse.Content.CopyToAsync(tempFileStream); 173 | var destinationDir = commandDirectory.GetDirectoryForPackage(nugetVersion.PackageName, nugetVersion.Version); 174 | var directoryExists = Directory.Exists(destinationDir); 175 | if (force && directoryExists) 176 | Directory.Delete(destinationDir, true); 177 | var shouldExtract = force || !directoryExists; 178 | if (shouldExtract) 179 | { 180 | WriteLineIfVerbose($"Extracting to '{destinationDir}'."); 181 | System.IO.Compression.ZipFile.ExtractToDirectory(tempFilePath, destinationDir); 182 | foreach (var fileToRename in Directory.EnumerateFiles(destinationDir, "*.removeext", SearchOption.AllDirectories)) 183 | File.Move(fileToRename, fileToRename.Substring(0, fileToRename.Length - ".removeext".Length)); 184 | } 185 | else 186 | { 187 | WriteLineIfVerbose($"Directory '{destinationDir}' already exists."); 188 | } 189 | var packageInfo = await PackageInfo.GetMainFilePathAsync(nugetVersion.PackageName, destinationDir); 190 | return packageInfo; 191 | } 192 | 193 | public void Dispose() 194 | { 195 | foreach (var sourceInfo in sourceInfos) 196 | sourceInfo.Dispose(); 197 | } 198 | 199 | private class Feed 200 | { 201 | [JsonProperty("resources")] 202 | public IList Resources { get; set; } 203 | } 204 | 205 | private class Resource 206 | { 207 | [JsonProperty("@id")] 208 | public string Id { get; set; } 209 | [JsonProperty("@type")] 210 | public string Type { get; set; } 211 | } 212 | 213 | private class Service 214 | { 215 | [JsonProperty("data")] 216 | public IList Data { get; set; } 217 | } 218 | 219 | private class ServiceData 220 | { 221 | [JsonProperty("id")] 222 | public string Id { get; set; } 223 | [JsonProperty("version")] 224 | public string Version { get; set; } 225 | [JsonProperty("versions")] 226 | public IList Versions { get; set; } 227 | } 228 | 229 | private class ServiceDataVersion 230 | { 231 | [JsonProperty("version")] 232 | public string Version { get; set; } 233 | } 234 | 235 | private class SourceInfo : IDisposable 236 | { 237 | private string FeedUrl { get; } 238 | 239 | public SourceInfo(PackageSource source) 240 | { 241 | Source = source; 242 | UpdateAuthorizationForClient(); 243 | FeedUrl = source.SourceUri.ToString(); 244 | } 245 | 246 | private Feed feed; 247 | public async Task GetFeedAsync() 248 | { 249 | if (feed != null) return feed; 250 | var feedResponse = await HttpClient.GetAsync(FeedUrl); 251 | if (!feedResponse.IsSuccessStatusCode) 252 | { 253 | WriteLine($"Could not get feed details from '{FeedUrl}'. Got status code '{feedResponse.StatusCode.ToString()}' ({(int)feedResponse.StatusCode})."); 254 | return null; 255 | } 256 | var feedContent = await feedResponse.Content.ReadAsStringAsync(); 257 | feed = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(feedContent)); 258 | return feed; 259 | } 260 | 261 | public async Task GetPackageAsync(string packageName, SemanticVersion packageVersion, bool includePreRelease) 262 | { 263 | var currentFeed = await GetFeedAsync(); 264 | if (currentFeed == null) 265 | { 266 | WriteLine("Current feed is null. Returning."); 267 | return null; 268 | } 269 | var searchQueryServiceUrl = currentFeed.Resources.FirstOrDefault(r => r.Type == "SearchQueryService")?.Id; 270 | if (searchQueryServiceUrl == null) 271 | searchQueryServiceUrl = currentFeed.Resources.FirstOrDefault(r => r.Type == "SearchQueryService/3.0.0-rc")?.Id; 272 | string serviceUrl; 273 | bool supportsQueryById; 274 | if (searchQueryServiceUrl == null) 275 | { 276 | searchQueryServiceUrl = currentFeed.Resources.FirstOrDefault(r => r.Type == "SearchQueryService/3.0.0-beta")?.Id; //vsts is still in this version 277 | if (searchQueryServiceUrl == null) 278 | { 279 | WriteLine("Nuget server does not offer a search query service we can work with."); 280 | return null; 281 | } 282 | serviceUrl = $"{searchQueryServiceUrl}?q={packageName}{(includePreRelease ? "&prerelease=true" : "")}"; 283 | supportsQueryById = false; 284 | } 285 | else 286 | { 287 | serviceUrl = $"{searchQueryServiceUrl}?q=packageid:{packageName}{(includePreRelease ? "&prerelease=true" : "")}"; 288 | supportsQueryById = true; 289 | } 290 | var serviceResponse = await HttpClient.GetAsync(serviceUrl); 291 | if (!serviceResponse.IsSuccessStatusCode) 292 | { 293 | WriteLine($"Could not get service details from '{serviceUrl}'."); 294 | WriteLineIfVerbose($"Got status code: '{(int)serviceResponse.StatusCode}' ({serviceResponse.StatusCode})."); 295 | throw new Exception($"Could not get service details from '{serviceUrl}'."); 296 | } 297 | var serviceContent = await serviceResponse.Content.ReadAsStringAsync(); 298 | var service = await Task.Factory.StartNew(() => JsonConvert.DeserializeObject(serviceContent)); 299 | var serviceData = supportsQueryById 300 | ? service.Data.FirstOrDefault() 301 | : service.Data.FirstOrDefault(sd => string.Compare(sd.Id, packageName, true) == 0); 302 | var latest = serviceData?.Version; 303 | if (latest == null) 304 | { 305 | WriteLineIfVerbose($"There was no package info for '{packageName}' on '{Source.Name}'."); 306 | return null; 307 | } 308 | WriteLineIfVerbose($"Found package '{packageName}' with latest version {latest}."); 309 | var currentSemanticVersion = SemanticVersion.Parse(latest); 310 | if (packageVersion != null) 311 | { 312 | var versionExists = serviceData.Versions.Any(v => v.Version == packageVersion.ToString()); 313 | if (versionExists) 314 | { 315 | currentSemanticVersion = packageVersion; 316 | } 317 | else 318 | { 319 | WriteLineIfVerbose($"Version '{packageVersion.ToString()}' was not found for '{packageName}' on '{Source.Name}'."); 320 | return null; 321 | } 322 | } 323 | var nugetVersion = new NugetVersion(currentSemanticVersion, this, serviceData.Id); 324 | return nugetVersion; 325 | } 326 | 327 | public PackageSource Source { get; set; } 328 | public HttpClient HttpClient { get; } = new HttpClient(); 329 | 330 | private void UpdateAuthorizationForClient() 331 | { 332 | if (Source.Credentials != null) 333 | { 334 | string password = null; 335 | if (Source.Credentials.IsPasswordClearText) 336 | { 337 | password = Source.Credentials.PasswordText; 338 | } 339 | else 340 | { 341 | try 342 | { 343 | password = Source.Credentials.Password; 344 | } 345 | catch (Exception ex) 346 | { 347 | WriteLine($"Could not get password for source '{Source.Name}'."); 348 | WriteLineIfVerbose(ex.ToString()); 349 | throw; 350 | } 351 | } 352 | HttpClient.DefaultRequestHeaders.Authorization = 353 | new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Source.Credentials.Username}:{password}"))); 354 | } 355 | else 356 | { 357 | HttpClient.DefaultRequestHeaders.Authorization = null; 358 | } 359 | } 360 | 361 | public async Task GetNupkgAsync(SemanticVersion semanticVersion, string packageName) 362 | { 363 | var packageBaseAddressUrl = feed.Resources.Last(r => r.Type.StartsWith("PackageBaseAddress")).Id; 364 | var version = semanticVersion.ToString(); 365 | var serviceUrl = $"{packageBaseAddressUrl}{packageName.ToLower()}/{version.ToLower()}/{packageName.ToLower()}.{version.ToLower()}.nupkg"; 366 | WriteLineIfVerbose($"Nupkg url is '{serviceUrl}'."); 367 | var originalUri = new Uri(serviceUrl); 368 | var url = serviceUrl; 369 | var redirects = 0; 370 | using (var localClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false })) 371 | { 372 | while (true) 373 | { 374 | if (redirects > 20) 375 | { 376 | WriteLineIfVerbose($"Exceeded maximum number of redirects."); 377 | return null; 378 | } 379 | localClient.DefaultRequestHeaders.Authorization = originalUri.Host == new Uri(url).Host 380 | ? HttpClient.DefaultRequestHeaders.Authorization 381 | : null; 382 | WriteLineIfVerbose($"Downloading nupkg from '{url}'..."); 383 | var serviceResponse = await localClient.GetAsync(url); 384 | if (serviceResponse.StatusCode == System.Net.HttpStatusCode.Redirect 385 | || serviceResponse.StatusCode == System.Net.HttpStatusCode.MovedPermanently 386 | || serviceResponse.StatusCode == System.Net.HttpStatusCode.RedirectMethod) 387 | { 388 | url = serviceResponse.Headers.Location.ToString(); 389 | WriteLineIfVerbose($"Redirected to '{url}'."); 390 | redirects++; 391 | } 392 | else 393 | { 394 | if (!serviceResponse.IsSuccessStatusCode) 395 | WriteLineIfVerbose($"Got response code {(int)serviceResponse.StatusCode} ({serviceResponse.StatusCode}) when accessing '{url}'..."); 396 | return serviceResponse; 397 | } 398 | } 399 | } 400 | } 401 | 402 | public void Dispose() => HttpClient.Dispose(); 403 | } 404 | 405 | private class NugetVersion 406 | { 407 | public NugetVersion(SemanticVersion version, SourceInfo info, string packageName) 408 | { 409 | Version = version; 410 | SourceInfo = info; 411 | PackageName = packageName; 412 | } 413 | 414 | public Task GetNupkgAsync(string packageName) => SourceInfo.GetNupkgAsync(Version, packageName); 415 | 416 | public SemanticVersion Version { get; } 417 | public SourceInfo SourceInfo { get; } 418 | public string PackageName { get; } 419 | } 420 | } 421 | 422 | 423 | public class MachineWideSettings : IMachineWideSettings 424 | { 425 | public MachineWideSettings() 426 | { 427 | var baseDirectory = NuGetEnvironment.GetFolderPath(NuGetFolderPath.MachineWideConfigDirectory); 428 | Settings = NuGet.Configuration.Settings.LoadMachineWideSettings(baseDirectory); 429 | } 430 | 431 | public IEnumerable Settings { get; private set; } 432 | } 433 | } -------------------------------------------------------------------------------- /src/dotnet-commands/PackageCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetCommands 4 | { 5 | public class PackageCommand : IComparable 6 | { 7 | public string Name { get; set; } 8 | public string ExecutableFilePath { get; set; } 9 | 10 | int IComparable.CompareTo(PackageCommand other) => 11 | string.Compare(Name, other.Name); 12 | } 13 | } -------------------------------------------------------------------------------- /src/dotnet-commands/PackageInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | using System.Threading.Tasks; 9 | using static DotNetCommands.Logger; 10 | 11 | namespace DotNetCommands 12 | { 13 | public class PackageInfo 14 | { 15 | private PackageInfo() { } 16 | public string PackageDir { get; private set; } 17 | public IList Commands { get; set; } 18 | 19 | //todo: refactor. Desperately. This is too long, and is doing too much. So I added lots of comments. For now, it is good enough, as we have tests! :) 20 | public static async Task GetMainFilePathAsync(string packageName, string packageDir) 21 | { 22 | var commandMetadataTextFilePath = Path.Combine(packageDir, "content", "commandMetadata.json"); 23 | var commands = new List(); 24 | if (File.Exists(commandMetadataTextFilePath)) //if we have a command metadata file, use it 25 | { 26 | string commandMetadataText; 27 | try 28 | { 29 | commandMetadataText = File.ReadAllText(commandMetadataTextFilePath); 30 | } 31 | catch (Exception ex) 32 | { 33 | WriteLine($"Could not read command metadata file '{commandMetadataTextFilePath}'."); 34 | WriteLineIfVerbose(ex.ToString()); 35 | return null; 36 | } 37 | try 38 | { 39 | dynamic commandMetadata = await Task.Factory.StartNew(() => JObject.Parse(commandMetadataText)); 40 | //command metadata can have a main value, or can have commands, here we take care of this difference 41 | //first for main 42 | if (commandMetadata.main != null) 43 | { 44 | if (!packageName.StartsWith("dotnet-")) 45 | { 46 | WriteLine($"If using 'main', the package name must start with dotnet-."); 47 | return null; 48 | } 49 | commands.Add(new PackageCommand 50 | { 51 | ExecutableFilePath = (string)commandMetadata.main, 52 | Name = packageName 53 | }); 54 | } 55 | //then for commands 56 | else if (commandMetadata.commands != null) 57 | { 58 | foreach (JProperty command in commandMetadata.commands) 59 | { 60 | var commandName = command.Name; 61 | if (!commandName.StartsWith("dotnet-")) 62 | { 63 | WriteLine($"Found command with name that does not start with dotnet-. Skipping."); 64 | continue; 65 | } 66 | if (command.Value.Type != JTokenType.String) 67 | { 68 | WriteLine($"JSON for metadata on file '{commandMetadataTextFilePath}' is not in a suitable format."); 69 | return null; 70 | } 71 | commands.Add(new PackageCommand 72 | { 73 | ExecutableFilePath = (string)command.Value, 74 | Name = commandName 75 | }); 76 | } 77 | } 78 | else 79 | { 80 | WriteLine($"JSON for metadata on file '{commandMetadataTextFilePath}' is not in a suitable format."); 81 | return null; 82 | } 83 | } 84 | catch (Exception ex) 85 | { 86 | WriteLine($"Could not decode Json for '{commandMetadataTextFilePath}'."); 87 | WriteLineIfVerbose(ex.ToString()); 88 | return null; 89 | } 90 | //at the end we normalize the extensions for each command 91 | //the command author should supply both the extension (Windows) and the non extension (Linux) files 92 | foreach (var command in commands) 93 | { 94 | var extension = Path.GetExtension(command.ExecutableFilePath); 95 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 96 | { 97 | if (string.IsNullOrWhiteSpace(extension)) 98 | { 99 | //if the command author did not add the .cmd extension to the executable, and we are on windows, we do it here 100 | command.ExecutableFilePath = $"{command.ExecutableFilePath}.cmd"; 101 | } 102 | } 103 | else 104 | { 105 | //if the command author did add the .cmd, .ps1 or .exe extension to the executable, and we are on Linux or Mac (not Windows), we remove the extension 106 | if (extension.Equals(".exe", StringComparison.CurrentCultureIgnoreCase) 107 | || extension.Equals(".cmd", StringComparison.CurrentCultureIgnoreCase) 108 | || extension.Equals(".ps1", StringComparison.CurrentCultureIgnoreCase)) 109 | { 110 | command.ExecutableFilePath = Path.Combine(Path.GetDirectoryName(command.ExecutableFilePath), 111 | Path.GetFileNameWithoutExtension(command.ExecutableFilePath)); 112 | } 113 | } 114 | } 115 | } 116 | else 117 | {//here the command author did not specify a command metadata file, so we check directly the tools dir 118 | var toolsDir = Path.Combine(packageDir, "tools"); 119 | WriteLineIfVerbose($"Tools dir is '{toolsDir}'."); 120 | if (!Directory.Exists(toolsDir)) 121 | { 122 | WriteLine("This package does not have a tools directory."); 123 | return null; 124 | } 125 | //we search for all files, according to platform 126 | var files = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 127 | ? Directory.EnumerateFiles(toolsDir, "*.exe") 128 | .Union(Directory.EnumerateFiles(toolsDir, "*.cmd")) 129 | .Union(Directory.EnumerateFiles(toolsDir, "*.ps1")).ToList() 130 | : Directory.EnumerateFiles(toolsDir).Where(fileName => string.IsNullOrEmpty(Path.GetExtension(fileName))).ToList(); 131 | if (IsVerbose) 132 | { 133 | foreach (var file in files) 134 | WriteLine($"Found tool file '{file}'."); 135 | } 136 | switch (files.Count) 137 | { 138 | case 0:// if we can't find any file, we have a problem 139 | { 140 | WriteLine("This package does not offer any executable."); 141 | return null; 142 | } 143 | case 1://if we only have one file, than we have a special case, we can use the command name if it starts with dotnet-* 144 | { 145 | var fullFilePath = files.First(); 146 | var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullFilePath); 147 | if (!fileNameWithoutExtension.StartsWith("dotnet-") && !packageName.StartsWith("dotnet-")) 148 | {//if neither the command nor the package start with dotnet-, then we have a problem 149 | WriteLine("This package does not offer any way to be linked to .NET CLI, it needs to either be named dotnet-* or offer a tool with such a name."); 150 | return null; 151 | } 152 | //if the tool name starts with dotnet-, we use it, otherwise we use the package name. 153 | //tool name is getting preference. is it correct? 154 | var commandName = fileNameWithoutExtension.StartsWith("dotnet-") 155 | ? fileNameWithoutExtension 156 | : packageName; 157 | commands.Add(new PackageCommand 158 | { 159 | ExecutableFilePath = fullFilePath, 160 | Name = commandName 161 | }); 162 | break; 163 | } 164 | default: 165 | {//here we have more than one tool, we can only take the ones that start with dotnet-* as we don't have a metadata file 166 | foreach (var fullFilePath in files) 167 | { 168 | var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullFilePath); 169 | var commandName = fileNameWithoutExtension.StartsWith("dotnet-") 170 | ? fileNameWithoutExtension 171 | : packageName; 172 | commands.Add(new PackageCommand 173 | { 174 | ExecutableFilePath = fullFilePath, 175 | Name = commandName 176 | }); 177 | } 178 | commands = commands.Distinct().ToList();//we need distinct because we could have 179 | //more than one tool that does not start with dotnet-* and the package could be names dotnet-*, 180 | //then we would end up several tools with the same name 181 | break; 182 | } 183 | } 184 | } 185 | if (!commands.Any()) 186 | { 187 | WriteLine("This package does not offer any way to be linked to .NET CLI, it needs to either be named dotnet-* or offer a tool with such a name."); 188 | return null; 189 | } 190 | foreach (var command in commands)//here we add the full path to all commands 191 | command.ExecutableFilePath = Path.GetFullPath(Path.Combine(packageDir, command.ExecutableFilePath)); 192 | var pi = new PackageInfo 193 | { 194 | PackageDir = packageDir, 195 | Commands = commands 196 | }; 197 | return pi; 198 | } 199 | 200 | private class CommandMetadataWithMain 201 | { 202 | [JsonProperty("main")] 203 | public string Main { get; set; } 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /src/dotnet-commands/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | using DocoptNet; 6 | using static DotNetCommands.Logger; 7 | using System.Runtime.InteropServices; 8 | using System.Threading.Tasks; 9 | 10 | namespace DotNetCommands 11 | { 12 | public class Program 13 | { 14 | public static int Main(string[] args) => RunAsync(args).Result; 15 | 16 | private async static Task RunAsync(string[] args) 17 | { 18 | const string usage = @".NET Commands 19 | 20 | Usage: 21 | dotnet commands install [@] [--force] [--pre] [--verbose] 22 | dotnet commands uninstall [ --verbose] 23 | dotnet commands update ( | all) [--pre] [--verbose] 24 | dotnet commands (list|ls) [--verbose] 25 | dotnet commands --help 26 | dotnet commands --version 27 | 28 | Options: 29 | --force Installs even if package was already installed. Optional. 30 | --pre Include pre-release versions. Ignored if version is supplied. Optional. 31 | --verbose Verbose. Optional. 32 | --help -h Show this screen. 33 | --version -v Show version. 34 | 35 | "; 36 | var homeDir = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("userprofile"); 37 | var commandDirectory = new CommandDirectory(Path.Combine(homeDir, ".nuget", "commands")); 38 | if (args.Length == 1 && args[0] == "bootstrap") 39 | { 40 | var installer = new Installer(commandDirectory); 41 | var success = await installer.InstallAsync("dotnet-commands", null, force: true, includePreRelease: true); 42 | return (int)(success ? ExitCodes.Success : ExitCodes.BootstrapFailed); 43 | } 44 | var argsWithRun = args; 45 | if (args.Any() && args[0] != "commands") 46 | argsWithRun = new[] { "commands" }.Concat(args).ToArray(); 47 | var version = typeof(Program).GetTypeInfo().Assembly.GetCustomAttribute().InformationalVersion; 48 | var arguments = new Docopt().Apply(usage, argsWithRun, version: version, exit: true); 49 | var verbose = arguments["--verbose"].IsTrue; 50 | Logger.IsVerbose = verbose; 51 | var command = arguments[""]?.ToString(); 52 | if (IsVerbose) 53 | { 54 | WriteLine($".NET Commands running on {RuntimeInformation.OSDescription} on {RuntimeInformation.ProcessArchitecture} (system is {RuntimeInformation.OSArchitecture}) with framework {RuntimeInformation.FrameworkDescription}."); 55 | WriteLine($"Args: {string.Join(" ", args.Select(s => $"\"{s}\""))}"); 56 | } 57 | 58 | if (command == "dotnet-commands" && (arguments["install"].IsTrue || arguments["update"].IsTrue)) 59 | { 60 | WriteLineIfVerbose("Request to update .NET Commands."); 61 | var updater = new Updater(commandDirectory); 62 | var updateNeeded = await updater.IsUpdateNeededAsync(command, arguments["--pre"].IsTrue); 63 | var exitCode = updateNeeded == Updater.UpdateNeeded.Yes ? ExitCodes.StartUpdate : ExitCodes.Success; 64 | WriteLineIfVerbose($"Should update .NET Commands: {updateNeeded}, exit code is going to be {exitCode} ({(int)exitCode})."); 65 | return (int)exitCode; 66 | } 67 | if (arguments["install"].IsTrue) 68 | { 69 | var commandParts = command.Split('@'); 70 | NuGet.Versioning.SemanticVersion packageVersion = null; 71 | switch (commandParts.Length) 72 | { 73 | case 1: 74 | break; 75 | case 2: 76 | command = commandParts[0]; 77 | try 78 | { 79 | packageVersion = NuGet.Versioning.SemanticVersion.Parse(commandParts[1]); 80 | } 81 | catch (ArgumentException) 82 | { 83 | Console.WriteLine($"Invalid version.\n{usage}"); 84 | return (int)ExitCodes.InvalidVersion; 85 | } 86 | break; 87 | default: 88 | Console.WriteLine($"Invalid version.\n{usage}"); 89 | return (int)ExitCodes.InvalidVersion; 90 | } 91 | var installer = new Installer(commandDirectory); 92 | var success = await installer.InstallAsync(command, packageVersion, arguments["--force"].IsTrue, arguments["--pre"].IsTrue); 93 | return (int)(success ? ExitCodes.Success : ExitCodes.InstallFailed); 94 | } 95 | if (arguments["uninstall"].IsTrue) 96 | { 97 | if (command == "dotnet-commands") 98 | { 99 | WriteLine("Can't uninstall .NET Commands."); 100 | return (int)ExitCodes.CantUninstallDotNetCommands; 101 | } 102 | var uninstaller = new Uninstaller(commandDirectory); 103 | var success = await uninstaller.UninstallAsync(command); 104 | return (int)(success ? ExitCodes.Success : ExitCodes.UninstallFailed); 105 | } 106 | if (arguments["update"].IsTrue) 107 | { 108 | var updater = new Updater(commandDirectory); 109 | var updateResult = await updater.UpdateAsync(command, arguments["--force"].IsTrue, arguments["--pre"].IsTrue); 110 | switch (updateResult) 111 | { 112 | case Updater.UpdateResult.NotNeeded: 113 | case Updater.UpdateResult.Success: 114 | return (int)ExitCodes.Success; 115 | case Updater.UpdateResult.PackageNotFound: 116 | return (int)ExitCodes.PackageNotFound; 117 | case Updater.UpdateResult.CouldntUninstall: 118 | return (int)ExitCodes.CouldntUninstall; 119 | case Updater.UpdateResult.UninstalledAndNotReinstalled: 120 | return (int)ExitCodes.UninstalledAndNotReinstalled; 121 | } 122 | } 123 | if (arguments["list"].IsTrue || arguments["ls"].IsTrue) 124 | { 125 | var lister = new Lister(commandDirectory); 126 | var success = await lister.ListAsync(); 127 | return (int)(success ? ExitCodes.Success : ExitCodes.ListFailed); 128 | } 129 | return (int)ExitCodes.Success; 130 | } 131 | 132 | enum ExitCodes : byte // 0 and from 64-113 according to http://tldp.org/LDP/abs/html/exitcodes.html 133 | { 134 | Success = 0, 135 | PackageNotFound = 64, 136 | CouldntUninstall = 65, 137 | UninstalledAndNotReinstalled = 66, 138 | ListFailed = 67, 139 | BootstrapFailed = 68, 140 | InstallFailed = 69, 141 | UninstallFailed = 70, 142 | CantUninstallDotNetCommands = 71, 143 | InvalidVersion = 72, 144 | StartUpdate = 113 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /src/dotnet-commands/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "dotnet-commands": { 4 | "commandName": "Project", 5 | "commandLineArgs": "install dotnet-foo@1.0.1" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/dotnet-commands/Uninstaller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Threading.Tasks; 6 | using static DotNetCommands.Logger; 7 | 8 | namespace DotNetCommands 9 | { 10 | public class Uninstaller 11 | { 12 | private readonly CommandDirectory commandDirectory; 13 | 14 | public Uninstaller(CommandDirectory commandDirectory) 15 | { 16 | this.commandDirectory = commandDirectory; 17 | } 18 | 19 | public async Task UninstallAsync(string packageName) 20 | { 21 | var deleted = await DeleteRedirectFileAsync(packageName); 22 | if (!deleted) return false; 23 | return DeletePackageDirectory(packageName); 24 | } 25 | 26 | private async Task DeleteRedirectFileAsync(string packageName) 27 | { 28 | var packageDir = commandDirectory.GetDirectoryForPackage(packageName); 29 | if (!Directory.Exists(packageDir)) 30 | { 31 | WriteLine($"Package {packageName} is not installed."); 32 | return false; 33 | } 34 | var packageDirs = Directory.EnumerateDirectories(packageDir); 35 | foreach (var packageAndVersionDir in packageDirs) 36 | { 37 | var packageInfo = await PackageInfo.GetMainFilePathAsync(packageName, packageAndVersionDir); 38 | if (packageInfo == null || !packageInfo.Commands.Any()) return true; 39 | foreach (var command in packageInfo.Commands) 40 | { 41 | var binFile = commandDirectory.GetBinFile(command.Name + (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")); 42 | try 43 | { 44 | if (File.Exists(binFile)) 45 | { 46 | WriteLineIfVerbose($"Deleting bin file '{binFile}'."); 47 | File.Delete(binFile); 48 | } 49 | else 50 | { 51 | WriteLineIfVerbose($"Bin file '{binFile}' does not exist."); 52 | } 53 | } 54 | catch (Exception ex) 55 | { 56 | WriteLine($"Could not delete bin file '{binFile}'."); 57 | WriteLineIfVerbose(ex.ToString()); 58 | return false; 59 | } 60 | } 61 | } 62 | return true; 63 | } 64 | 65 | private bool DeletePackageDirectory(string packageName) 66 | { 67 | var packageDir = commandDirectory.GetDirectoryForPackage(packageName); 68 | var parent = Directory.GetParent(packageDir); 69 | var movedPackageDir = Path.Combine(parent.ToString(), $"{packageName}_{Guid.NewGuid().ToString().Replace("-", "").Substring(0, 5)}.safetodelete.tmp"); 70 | try 71 | { 72 | //try to move, the package could be in use 73 | WriteLineIfVerbose($"Moving '{packageDir}' to '{movedPackageDir}' to later delete it."); 74 | Directory.Move(packageDir, movedPackageDir); 75 | } 76 | catch (Exception ex) 77 | { 78 | WriteLine($"Could not delete package '{packageDir}', it is probably in use or you do not have permission."); 79 | WriteLineIfVerbose(ex.ToString()); 80 | return false; 81 | } 82 | try 83 | { 84 | WriteLineIfVerbose($"Deleting '{movedPackageDir}'."); 85 | Directory.Delete(movedPackageDir, true); 86 | } 87 | catch (Exception ex) 88 | { 89 | WriteLine($"Could not delete the moved package for '{packageDir}'. This is not expected and should not happen."); 90 | WriteLineIfVerbose(ex.ToString()); 91 | } 92 | return true; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/dotnet-commands/Updater.cs: -------------------------------------------------------------------------------- 1 | using NuGet.Versioning; 2 | using System; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using static DotNetCommands.Logger; 7 | 8 | namespace DotNetCommands 9 | { 10 | public class Updater 11 | { 12 | private readonly CommandDirectory commandDirectory; 13 | 14 | public Updater(CommandDirectory commandDirectory) 15 | { 16 | this.commandDirectory = commandDirectory; 17 | } 18 | 19 | public async Task UpdateAsync(string packageName, bool force, bool includePreRelease) 20 | { 21 | var updateNeeded = await IsUpdateNeededAsync(packageName, includePreRelease); 22 | if (updateNeeded == UpdateNeeded.No) return UpdateResult.NotNeeded; 23 | if (updateNeeded == UpdateNeeded.PackageNotFound) return UpdateResult.PackageNotFound; 24 | var uninstaller = new Uninstaller(commandDirectory); 25 | var uninstalled = await uninstaller.UninstallAsync(packageName); 26 | if (!uninstalled) return UpdateResult.CouldntUninstall; 27 | var installer = new Installer(commandDirectory); 28 | var installed = await installer.InstallAsync(packageName, null, force, includePreRelease); 29 | return installed ? UpdateResult.Success : UpdateResult.UninstalledAndNotReinstalled; 30 | } 31 | 32 | public async Task IsUpdateNeededAsync(string packageName, bool includePreRelease) 33 | { 34 | SemanticVersion largestAvailableVersion; 35 | try 36 | { 37 | using (var downloader = new NugetDownloader(commandDirectory)) 38 | { 39 | var versionFound = await downloader.GetLatestVersionAsync(packageName, includePreRelease); 40 | if (versionFound == null) 41 | { 42 | WriteLineIfVerbose($"Could not find '{packageName}'."); 43 | return UpdateNeeded.PackageNotFound; 44 | } 45 | largestAvailableVersion = SemanticVersion.Parse(versionFound); 46 | } 47 | } 48 | catch (Exception ex) 49 | { 50 | WriteLine("Could not download Nuget."); 51 | WriteLineIfVerbose(ex.ToString()); 52 | return UpdateNeeded.No; 53 | } 54 | var directory = commandDirectory.GetDirectoryForPackage(packageName); 55 | var packageDirs = Directory.EnumerateDirectories(directory); 56 | var packageVersions = packageDirs.Select(packageDir => SemanticVersion.Parse(Path.GetFileName(packageDir))); 57 | var largestInstalledVersion = packageVersions.Max(); 58 | return largestInstalledVersion >= largestAvailableVersion ? UpdateNeeded.No : UpdateNeeded.Yes; 59 | } 60 | 61 | public enum UpdateResult 62 | { 63 | Success, NotNeeded, PackageNotFound, CouldntUninstall, UninstalledAndNotReinstalled 64 | } 65 | public enum UpdateNeeded 66 | { 67 | Yes, No, PackageNotFound 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/dotnet-commands/ZipPublish.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | Push-Location $PSScriptRoot\bin\Debug\netcoreapp1.0\publish 3 | if (gcm tar -ErrorAction Ignore) { 4 | $gzip = $([System.IO.Path]::GetFullPath("$(pwd)\..\dotnet-commands.tar.gz")) 5 | echo "Creating '$gzip'..." 6 | if (Test-Path $gzip) { rm $gzip } 7 | tar -cvzf ../dotnet-commands.tar.gz . > $null 8 | if ($LASTEXITCODE -ne 0) { 9 | echo "Error publishing." 10 | Pop-Location 11 | exit 1 12 | } 13 | } 14 | Add-Type -assembly System.IO.Compression.FileSystem 15 | $zip = $([System.IO.Path]::GetFullPath("$(pwd)\..\dotnet-commands.zip")) 16 | echo "Creating '$zip'..." 17 | if (Test-Path $zip) { rm $zip } 18 | [System.IO.Compression.ZipFile]::CreateFromDirectory($(pwd), "$(pwd)\..\dotnet-commands.zip") 19 | Pop-Location -------------------------------------------------------------------------------- /src/dotnet-commands/commandMetadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "lib/netcoreapp1.0/dotnet-commands.cmd" 3 | } -------------------------------------------------------------------------------- /src/dotnet-commands/docker.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(ParameterSetName='build', Mandatory=$true)][switch]$build, 3 | [Parameter(ParameterSetName='run', Mandatory=$true)][switch]$run 4 | ) 5 | if ($build) { 6 | docker build --tag giggio/dotnetdev $PSScriptRoot 7 | } 8 | if ($run) { 9 | $id=$(docker ps -f name=dotnet-commands -q) 10 | if ($id) { 11 | echo "Already running, attaching..." 12 | docker attach $id 13 | exit 14 | } 15 | $id=$(docker ps -f name=dotnet-commands -a -q) 16 | if ($id) { 17 | echo "Already created, starting and attaching..." 18 | docker start --attach --interactive $id 19 | } else { 20 | echo "Creating..." 21 | docker run -ti --name dotnet-commands -v "${PSScriptRoot}\..\..\:/app" giggio/dotnetdev 22 | } 23 | } -------------------------------------------------------------------------------- /src/dotnet-commands/dotnet-commands: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | dotnet $DIR/dotnet-commands.dll "$@" 5 | EL=$? 6 | echo $EL 7 | if [ $EL -eq 113 ]; then 8 | echo foo 9 | \curl -sSL https://raw.githubusercontent.com/Lambda3/dotnet-commands/master/src/dotnet-commands/install.sh | bash 10 | exit $? 11 | else 12 | echo bar 13 | exit $EL 14 | fi -------------------------------------------------------------------------------- /src/dotnet-commands/dotnet-commands.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | dotnet %~dp0\dotnet-commands.dll %* 3 | set EL=%ERRORLEVEL% 4 | IF %EL% EQU 113 ( 5 | powershell -NoProfile -ExecutionPolicy unrestricted -Command "&{$wc=New-Object System.Net.WebClient;$wc.Proxy=[System.Net.WebRequest]::DefaultWebProxy;$wc.Proxy.Credentials=[System.Net.CredentialCache]::DefaultNetworkCredentials;Invoke-Expression($wc.DownloadString('https://raw.githubusercontent.com/Lambda3/dotnet-commands/master/src/dotnet-commands/install.ps1'))}" 6 | ) else ( 7 | exit %EL% 8 | ) -------------------------------------------------------------------------------- /src/dotnet-commands/dotnet-commands.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 1917c2da-feb8-4128-9a22-17dcc9f84af4 10 | DotNetCommands 11 | .\obj 12 | .\bin\ 13 | 14 | 15 | 2.0 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/dotnet-commands/install.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | $releases = Invoke-WebRequest -UseBasicParsing https://github.com/Lambda3/dotnet-commands/releases.atom 3 | $uri="https://github.com/Lambda3/dotnet-commands/releases/download/$($([xml]$releases.Content).feed.entry[0].title)/dotnet-commands.zip" 4 | $outFile=[System.IO.Path]::GetTempFileName() 5 | Invoke-WebRequest -UseBasicParsing -uri $uri -OutFile $outFile 6 | Add-Type -assembly System.IO.Compression.FileSystem 7 | $outDir = "$outFile-extracted" 8 | [System.IO.Compression.ZipFile]::ExtractToDirectory($outFile, $outDir) 9 | Write-Host $outDir 10 | . "$outDir\dotnet-commands.cmd" bootstrap 11 | if ($LASTEXITCODE -ne 0) { 12 | Write-Host "Could not install." 13 | exit 1 14 | } 15 | $path = [Environment]::GetEnvironmentVariable("PATH", "User") 16 | if ($path -notlike "*$env:USERPROFILE\.nuget\commands\bin;*") { 17 | $newPath = "$env:USERPROFILE\.nuget\commands\bin;$path" 18 | [Environment]::SetEnvironmentVariable("PATH", $newPath, "User") 19 | $env:Path="$env:USERPROFILE\.nuget\commands\bin;$env:Path" 20 | } 21 | -------------------------------------------------------------------------------- /src/dotnet-commands/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | releases=$(curl https://github.com/Lambda3/dotnet-commands/releases.atom) 3 | uri="https://github.com/Lambda3/dotnet-commands/releases/download/$(echo $releases | grep -oPm1 "(?<=)[^<]+" | sed -n 2p)/dotnet-commands.tar.gz" 4 | if command -v tempfile >/dev/null 2>&1; then 5 | outFile=`tempfile` 6 | else 7 | outFile=`mktemp` 8 | fi 9 | curl -o $outFile -L $uri 10 | outDir="$outFile-extracted" 11 | echo $outDir 12 | mkdir $outDir 13 | tar -xvzf $outFile -C $outDir 14 | echo $outDir 15 | . "$outDir/dotnet-commands" bootstrap 16 | if [ $? -ne 0 ]; then 17 | echo "Could not install." 18 | exit 1 19 | fi 20 | if [[ $PATH != *"$HOME/.nuget/commands/bin"* ]]; then 21 | echo "PATH=$HOME/.nuget/commands/bin:$PATH" >> $HOME/.bashrc 22 | echo "Your .bashrc was updated, either start a new bash instance from scratch or \`source ~/.bashrc\`." 23 | fi -------------------------------------------------------------------------------- /src/dotnet-commands/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.5.2", 3 | "description": "A .NET CLI Commands library.", 4 | "dependencies": { 5 | "Microsoft.NETCore.App": { 6 | "type": "platform", 7 | "version": "1.0.0" 8 | }, 9 | "docopt.net": "0.6.1.9", 10 | "System.Collections.NonGeneric": "4.0.1", 11 | "Newtonsoft.Json": "9.0.1", 12 | "System.IO.Compression.ZipFile": "4.0.1", 13 | "NuGet.Versioning": "3.5.0-rc1-final", 14 | "NuGet.Configuration": "3.5.0-rc1-final" 15 | }, 16 | "authors": [ "giggio" ], 17 | "packOptions": { 18 | "owners": [ "giggio" ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/Lambda3/dotnet-commands" 22 | }, 23 | "files": { 24 | "mappings": { 25 | "content/commandMetadata.json": "commandMetadata.json", 26 | "lib/netcoreapp1.0/dotnet-commands.cmd": "dotnet-commands.cmd", 27 | "lib/netcoreapp1.0/dotnet-commands": "dotnet-commands", 28 | "lib/netcoreapp1.0/project.json": "project.json", 29 | "lib/netcoreapp1.0/project.lock.json": "project.lock.json", 30 | "lib/netcoreapp1.0/dotnet-commands.deps.json": "bin/**/netcoreapp1.0/dotnet-commands.deps.json" 31 | } 32 | } 33 | }, 34 | "publishOptions": { 35 | "includeFiles": [ 36 | "dotnet-commands.cmd", 37 | "dotnet-commands" 38 | ] 39 | }, 40 | "buildOptions": { 41 | "debugType": "portable", 42 | "emitEntryPoint": true, 43 | "copyToOutput": { 44 | "includeFiles": [ 45 | "dotnet-commands.cmd", 46 | "dotnet-commands" 47 | ] 48 | } 49 | }, 50 | "frameworks": { 51 | "netcoreapp1.0": { 52 | "imports": [ 53 | "dnxcore50", 54 | "portable-net45+win8" 55 | ] 56 | } 57 | }, 58 | "scripts": { 59 | "postpublish": [ "powershell -noprofile \"& .\\ZipPublish.ps1\"" ] 60 | } 61 | } -------------------------------------------------------------------------------- /src/dotnet-foo/dotnet-foo.cmd: -------------------------------------------------------------------------------- 1 | @echo Hello from .NET -------------------------------------------------------------------------------- /src/dotnet-foo/dotnet-foo.removeext: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo Hello from .NET -------------------------------------------------------------------------------- /src/dotnet-foo/dotnet-foo.xproj: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 3 | <PropertyGroup> 4 | <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion> 5 | <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> 6 | </PropertyGroup> 7 | <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> 8 | <PropertyGroup Label="Globals"> 9 | <ProjectGuid>474de765-dd8a-4aa6-8099-eca92ff38d79</ProjectGuid> 10 | <RootNamespace>dotnet-foo</RootNamespace> 11 | <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath> 12 | <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> 13 | </PropertyGroup> 14 | 15 | <PropertyGroup> 16 | <SchemaVersion>2.0</SchemaVersion> 17 | </PropertyGroup> 18 | <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> 19 | </Project> -------------------------------------------------------------------------------- /src/dotnet-foo/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.2-*", 3 | "description": "A sample package only to validate that the tools installer works.", 4 | "dependencies": { 5 | "Microsoft.NETCore": "5.0.2" 6 | }, 7 | "authors": [ "giggio" ], 8 | "packOptions": { 9 | "owners": [ 10 | "giggio" 11 | ], 12 | "files": { 13 | "mappings": { 14 | "tools/dotnet-foo.cmd": "dotnet-foo.cmd", 15 | "tools/dotnet-foo.removeext": "dotnet-foo.removeext" 16 | } 17 | } 18 | }, 19 | "frameworks": { 20 | "netcoreapp1.0": {} 21 | } 22 | } -------------------------------------------------------------------------------- /test/IntegrationTests/AssemblyInit.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Runtime.InteropServices; 4 | using static DotNetCommands.Logger; 5 | 6 | [assembly: LevelOfParallelism(1)] 7 | namespace IntegrationTests 8 | { 9 | [SetUpFixture] 10 | public class AssemblyInit 11 | { 12 | [OneTimeSetUp] 13 | public static void AssemblyInitialize() 14 | { 15 | IsVerbose = true; 16 | SetLogger(msg => Console.WriteLine($"[Tool {DateTime.Now.ToString("MM/dd/yy hh:mm:ss")}] {msg}")); 17 | WriteLineIfVerbose($".NET Commands running on {RuntimeInformation.OSDescription} on {RuntimeInformation.ProcessArchitecture} (system is {RuntimeInformation.OSArchitecture}) with framework {RuntimeInformation.FrameworkDescription}."); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/IntegrationTests/CommandDirectoryCleanup.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | 6 | namespace IntegrationTests 7 | { 8 | public sealed class CommandDirectoryCleanup : IDisposable 9 | { 10 | public CommandDirectory CommandDirectory { get; private set; } 11 | 12 | public CommandDirectoryCleanup() 13 | { 14 | CommandDirectory = new CommandDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString().Replace("-", "").Substring(0, 10))); 15 | } 16 | 17 | public void Dispose() 18 | { 19 | try 20 | { 21 | Directory.Delete(CommandDirectory.BaseDir, true); 22 | } 23 | catch (Exception ex) 24 | { 25 | Debug.WriteLine($"Could not delete the base dir '{CommandDirectory.BaseDir}'.\n{ex.ToString()}"); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /test/IntegrationTests/CommandDirectoryTests.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NuGet.Versioning; 4 | using NUnit.Framework; 5 | using System.IO; 6 | 7 | namespace IntegrationTests 8 | { 9 | [TestFixture] 10 | public class CommandDirectoryTests 11 | { 12 | private CommandDirectory commandDirectory; 13 | private string baseDir; 14 | 15 | [OneTimeSetUp] 16 | public void ClassInitialize() 17 | { 18 | baseDir = Path.Combine(Path.GetTempPath(), "foo") + Path.DirectorySeparatorChar; 19 | commandDirectory = new CommandDirectory(baseDir); 20 | } 21 | 22 | [Test] 23 | public void BaseDir() => commandDirectory.BaseDir.Should().Be(baseDir); 24 | 25 | [Test] 26 | public void BaseDirWithoutDirectorySeparatorCharGetsItAdded() => new CommandDirectory(baseDir.Substring(0, baseDir.Length - 1)).BaseDir.Should().Be(baseDir); 27 | 28 | [Test] 29 | public void GetBinFile() => commandDirectory.GetBinFile("foo.cmd").Should().Be(Path.Combine(baseDir, "bin", "foo.cmd")); 30 | 31 | [Test] 32 | public void GetDirectoryForPackage() => commandDirectory.GetDirectoryForPackage("foo", SemanticVersion.Parse("1.2.3")).Should().Be(Path.Combine(baseDir, "packages", "foo", "1.2.3")); 33 | 34 | [Test] 35 | public void MakeRelativeToBaseDir() => commandDirectory.MakeRelativeToBaseDir(Path.Combine(baseDir, "foo", "bar")).Should().Be(Path.Combine("..", "foo", "bar")); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/IntegrationTests/DownloadNugetTest.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace IntegrationTests 9 | { 10 | [TestFixture] 11 | public class DownloadNugetTest 12 | { 13 | private NugetDownloader downloader; 14 | private CommandDirectoryCleanup commandDirectoryCleanup; 15 | 16 | [OneTimeSetUp] 17 | public void ClassInitialize() 18 | { 19 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 20 | downloader = new NugetDownloader(commandDirectoryCleanup.CommandDirectory); 21 | } 22 | 23 | [OneTimeTearDown] 24 | public void ClassCleanup() 25 | { 26 | downloader.Dispose(); 27 | commandDirectoryCleanup.Dispose(); 28 | } 29 | 30 | [Test, Retry] 31 | public async Task DownloadDotNetFooAsync() 32 | { 33 | var packageInfo = await downloader.DownloadAndExtractNugetAsync("dotnet-foo", null, force: false, includePreRelease: false); 34 | Directory.Exists(packageInfo.PackageDir).Should().BeTrue(); 35 | var cmd = Directory.EnumerateFiles(packageInfo.PackageDir, "dotnet-foo.cmd", SearchOption.AllDirectories).FirstOrDefault(); 36 | cmd.Should().NotBeNull(); 37 | Directory.GetParent(cmd).Name.Should().Be("tools"); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /test/IntegrationTests/DownloadNugetTestWithoutPackageSources.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using System; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using static IntegrationTests.Retrier; 9 | 10 | namespace IntegrationTests 11 | { 12 | [TestFixture] 13 | public class DownloadNugetTestWithoutPackageSources 14 | { 15 | private NugetDownloader downloader; 16 | private CommandDirectoryCleanup commandDirectoryCleanup; 17 | private string tempPath; 18 | private string currentDirectoryWhenTestStarted; 19 | 20 | [OneTimeSetUp] 21 | public void OneTimeSetUp() 22 | { 23 | currentDirectoryWhenTestStarted = Directory.GetCurrentDirectory(); 24 | tempPath = Path.Combine(Path.GetTempPath(), "DotNetTempPath" + Guid.NewGuid().ToString().Replace("-", "").Substring(0, 10)); 25 | Directory.CreateDirectory(tempPath); 26 | Directory.SetCurrentDirectory(tempPath); 27 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 28 | downloader = new NugetDownloader(commandDirectoryCleanup.CommandDirectory); 29 | } 30 | 31 | [OneTimeTearDown] 32 | public void ClassCleanup() 33 | { 34 | Directory.SetCurrentDirectory(currentDirectoryWhenTestStarted); 35 | downloader.Dispose(); 36 | commandDirectoryCleanup.Dispose(); 37 | try 38 | { 39 | Directory.Delete(tempPath, true); 40 | } 41 | #pragma warning disable CC0004 // Catch block cannot be empty 42 | catch { } 43 | #pragma warning restore CC0004 // Catch block cannot be empty 44 | } 45 | 46 | [Test, Retry] 47 | public async Task DownloadDotNetFooWithoutPackageSourcesAsync() 48 | { 49 | var packageInfo = await downloader.DownloadAndExtractNugetAsync("dotnet-foo", null, force: false, includePreRelease: false); 50 | Directory.Exists(packageInfo.PackageDir).Should().BeTrue(); 51 | var cmd = Directory.EnumerateFiles(packageInfo.PackageDir, "dotnet-foo.cmd", SearchOption.AllDirectories).FirstOrDefault(); 52 | cmd.Should().NotBeNull(); 53 | Directory.GetParent(cmd).Name.Should().Be("tools"); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /test/IntegrationTests/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "CC0061:Async method can be terminating with 'Async' name.", Justification = "Tests don't need to end in Async.")] -------------------------------------------------------------------------------- /test/IntegrationTests/InstallerTestForDotNetTool.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Threading.Tasks; 8 | using static IntegrationTests.Retrier; 9 | 10 | namespace IntegrationTests 11 | { 12 | [TestFixture] 13 | public class InstallerTestForDotNetTool 14 | { 15 | private const string packageName = "dotnet-baz"; 16 | private CommandDirectoryCleanup commandDirectoryCleanup; 17 | private Installer installer; 18 | private bool installed; 19 | private string baseDir; 20 | 21 | [OneTimeSetUp] 22 | public Task OneTimeSetUp() => RetryAsync(SetupAsync); 23 | 24 | public async Task SetupAsync() 25 | { 26 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 27 | baseDir = commandDirectoryCleanup.CommandDirectory.BaseDir; 28 | installer = new Installer(commandDirectoryCleanup.CommandDirectory); 29 | installed = await installer.InstallAsync(packageName, null, force: false, includePreRelease: true); 30 | installed.Should().BeTrue(); 31 | } 32 | 33 | [OneTimeTearDown] 34 | public void ClassCleanup() => commandDirectoryCleanup.Dispose(); 35 | 36 | [Test] 37 | public void WroteRedirectFile() => File.Exists(Path.Combine(baseDir, "bin", $"{packageName}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}")).Should().BeTrue(); 38 | 39 | [Test] 40 | public void CreatedRuntimeConfigDevJsonFileWithCorrectConfig() 41 | { 42 | var runtimeConfigFile = Directory.EnumerateFiles(Path.Combine(baseDir, "packages", packageName), $"{packageName}.runtimeconfig.dev.json", SearchOption.AllDirectories).SingleOrDefault(); 43 | runtimeConfigFile.Should().NotBeNull(); 44 | var parentDir = Directory.GetParent(runtimeConfigFile); 45 | parentDir.Name.Should().Be("netcoreapp1.0"); 46 | parentDir.Parent.Name.Should().Be("lib"); 47 | parentDir.Parent.Parent.Parent.Name.Should().Be(packageName); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/IntegrationTests/InstallerTestForGenericTool.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using System.Threading.Tasks; 7 | using static IntegrationTests.Retrier; 8 | 9 | namespace IntegrationTests 10 | { 11 | [TestFixture] 12 | public class InstallerTestForGenericTool 13 | { 14 | private const string packageName = "dotnet-FOO"; 15 | private const string packageNameCorrectCasing = "dotnet-foo"; 16 | private CommandDirectoryCleanup commandDirectoryCleanup; 17 | private Installer installer; 18 | private bool installed; 19 | private string baseDir; 20 | 21 | [OneTimeSetUp] 22 | public Task OneTimeSetUp() => RetryAsync(SetupAsync); 23 | 24 | public async Task SetupAsync() 25 | { 26 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 27 | baseDir = commandDirectoryCleanup.CommandDirectory.BaseDir; 28 | installer = new Installer(commandDirectoryCleanup.CommandDirectory); 29 | installed = await installer.InstallAsync(packageName, null, force: false, includePreRelease: false); 30 | installed.Should().BeTrue(); 31 | } 32 | 33 | [OneTimeTearDown] 34 | public void ClassCleanup() => commandDirectoryCleanup.Dispose(); 35 | 36 | [Test] 37 | public void WroteRedirectFileForWindows() 38 | { 39 | var wroteRedirectFile = File.Exists(Path.Combine(baseDir, "bin", $"{packageNameCorrectCasing}.cmd")); 40 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 41 | wroteRedirectFile.Should().BeTrue(); 42 | else 43 | wroteRedirectFile.Should().BeFalse(); 44 | } 45 | 46 | [Test] 47 | public void WroteRedirectFileForOtherPlatforms() 48 | { 49 | var wroteRedirectFile = File.Exists(Path.Combine(baseDir, "bin", packageNameCorrectCasing)); 50 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 51 | wroteRedirectFile.Should().BeTrue(); 52 | else 53 | wroteRedirectFile.Should().BeFalse(); 54 | } 55 | 56 | [Test] 57 | public void DidNotCreateRuntimeConfigDevJsonFileWithCorrectConfig() => 58 | Directory.EnumerateFiles(baseDir, "*.runtimeconfig.dev.json", SearchOption.AllDirectories).Should().BeEmpty(); 59 | 60 | [Test] 61 | public void PackageNameHasCorrectCasing() 62 | { 63 | var packageDir = commandDirectoryCleanup.CommandDirectory.GetDirectoryForPackage(packageNameCorrectCasing); 64 | Directory.Exists(packageDir).Should().BeTrue(); 65 | var di = new DirectoryInfo(packageDir); 66 | var properlyCasedName = di.Parent.GetFileSystemInfos(di.Name)[0].Name; 67 | packageNameCorrectCasing.Should().Be(properlyCasedName); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /test/IntegrationTests/InstallerTestForGenericToolWithSpecificVersion.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NuGet.Versioning; 4 | using NUnit.Framework; 5 | using System.IO; 6 | using System.Runtime.InteropServices; 7 | using System.Threading.Tasks; 8 | using static IntegrationTests.Retrier; 9 | 10 | namespace IntegrationTests 11 | { 12 | [TestFixture] 13 | public class InstallerTestForGenericToolWithSpecificVersion 14 | { 15 | private const string packageName = "dotnet-foo"; 16 | private CommandDirectoryCleanup commandDirectoryCleanup; 17 | private Installer installer; 18 | private bool installed; 19 | private string baseDir; 20 | 21 | [OneTimeSetUp] 22 | public Task OneTimeSetUp() => RetryAsync(SetupAsync); 23 | 24 | public async Task SetupAsync() 25 | { 26 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 27 | baseDir = commandDirectoryCleanup.CommandDirectory.BaseDir; 28 | installer = new Installer(commandDirectoryCleanup.CommandDirectory); 29 | installed = await installer.InstallAsync(packageName, SemanticVersion.Parse("1.0.1"), force: false, includePreRelease: false); 30 | installed.Should().BeTrue(); 31 | } 32 | 33 | [OneTimeTearDown] 34 | public void ClassCleanup() => commandDirectoryCleanup.Dispose(); 35 | 36 | [Test] 37 | public void WroteRedirectFileForWindows() 38 | { 39 | var wroteRedirectFile = File.Exists(Path.Combine(baseDir, "bin", $"{packageName}.cmd")); 40 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 41 | wroteRedirectFile.Should().BeTrue(); 42 | else 43 | wroteRedirectFile.Should().BeFalse(); 44 | } 45 | 46 | [Test] 47 | public void WroteRedirectFileForOtherPlatforms() 48 | { 49 | var wroteRedirectFile = File.Exists(Path.Combine(baseDir, "bin", packageName)); 50 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 51 | wroteRedirectFile.Should().BeTrue(); 52 | else 53 | wroteRedirectFile.Should().BeFalse(); 54 | } 55 | 56 | [Test] 57 | public void DidNotCreateRuntimeConfigDevJsonFileWithCorrectConfig() => 58 | Directory.EnumerateFiles(baseDir, "*.runtimeconfig.dev.json", SearchOption.AllDirectories).Should().BeEmpty(); 59 | 60 | [Test] 61 | public void InstalledCorrectPackageVersion() => 62 | Directory.Exists(commandDirectoryCleanup.CommandDirectory.GetDirectoryForPackage(packageName, SemanticVersion.Parse("1.0.1"))).Should().BeTrue(); 63 | } 64 | } -------------------------------------------------------------------------------- /test/IntegrationTests/InstallerTestForMultipleCommandsWithMetadata.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using System.Threading.Tasks; 7 | using static IntegrationTests.Retrier; 8 | 9 | namespace IntegrationTests 10 | { 11 | [TestFixture] 12 | public class InstallerTestForMultipleCommandsWithMetadata 13 | { 14 | private const string packageName = "dotnet-bar"; 15 | private CommandDirectoryCleanup commandDirectoryCleanup; 16 | private Installer installer; 17 | private bool installed; 18 | private string baseDir; 19 | 20 | [OneTimeSetUp] 21 | public Task OneTimeSetUp() => RetryAsync(SetupAsync); 22 | 23 | public async Task SetupAsync() 24 | { 25 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 26 | baseDir = commandDirectoryCleanup.CommandDirectory.BaseDir; 27 | installer = new Installer(commandDirectoryCleanup.CommandDirectory); 28 | installed = await installer.InstallAsync(packageName, null, force: false, includePreRelease: false); 29 | installed.Should().BeTrue(); 30 | } 31 | 32 | [OneTimeTearDown] 33 | public void ClassCleanup() => commandDirectoryCleanup.Dispose(); 34 | 35 | [Test] 36 | public void WroteRedirectFileForCommandA() => File.Exists(Path.Combine(baseDir, "bin", $"dotnet-bar-aa{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}")).Should().BeTrue(); 37 | 38 | [Test] 39 | public void WroteRedirectFileForCommandB() => File.Exists(Path.Combine(baseDir, "bin", $"dotnet-bar-bb{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}")).Should().BeTrue(); 40 | } 41 | } -------------------------------------------------------------------------------- /test/IntegrationTests/IntegrationTests.xproj: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 3 | <PropertyGroup> 4 | <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> 5 | <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> 6 | </PropertyGroup> 7 | <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> 8 | <PropertyGroup Label="Globals"> 9 | <ProjectGuid>1d1024db-9159-42dc-9e73-b797ae7f8e4c</ProjectGuid> 10 | <RootNamespace>IntegrationTests</RootNamespace> 11 | <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath> 12 | <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> 13 | <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion> 14 | </PropertyGroup> 15 | <PropertyGroup> 16 | <SchemaVersion>2.0</SchemaVersion> 17 | </PropertyGroup> 18 | <ItemGroup> 19 | <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" /> 20 | </ItemGroup> 21 | <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> 22 | </Project> -------------------------------------------------------------------------------- /test/IntegrationTests/ListerListsPackagesInstalled.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using Moq; 4 | using NUnit.Framework; 5 | using System; 6 | using System.Threading.Tasks; 7 | using static IntegrationTests.Retrier; 8 | 9 | namespace IntegrationTests 10 | { 11 | [TestFixture] 12 | public class ListerListsPackagesInstalled 13 | { 14 | private CommandDirectoryCleanup commandDirectoryCleanup; 15 | private Mock<Action<string>> logger; 16 | private bool isVerbose; 17 | 18 | [OneTimeSetUp] 19 | public Task OneTimeSetUp() => RetryAsync(SetupAsync); 20 | 21 | public async Task SetupAsync() 22 | { 23 | isVerbose = Logger.IsVerbose; 24 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 25 | var baseDir = commandDirectoryCleanup.CommandDirectory.BaseDir; 26 | var installer = new Installer(commandDirectoryCleanup.CommandDirectory); 27 | var installed = await installer.InstallAsync("dotnet-foo", null, force: false, includePreRelease: false); 28 | installed.Should().BeTrue(); 29 | installed = await installer.InstallAsync("dotnet-bar", null, force: false, includePreRelease: false); 30 | installed.Should().BeTrue(); 31 | logger = new Mock<Action<string>>(); 32 | Logger.IsVerbose = false; 33 | Logger.SetLogger(logger.Object); 34 | var lister = new Lister(commandDirectoryCleanup.CommandDirectory); 35 | await lister.ListAsync(); 36 | } 37 | 38 | [OneTimeTearDown] 39 | public void ClassCleanup() 40 | { 41 | Logger.IsVerbose = isVerbose; 42 | commandDirectoryCleanup.Dispose(); 43 | } 44 | 45 | [Test] 46 | public void ListedPackagesAndCommands() 47 | { 48 | #pragma warning disable CC0031 // Check for null before calling a delegate 49 | logger.Verify(l => l( 50 | $"dotnet-bar (1.0.2){Environment.NewLine} dotnet-bar-aa{Environment.NewLine} dotnet-bar-bb{Environment.NewLine}dotnet-foo (1.0.2){Environment.NewLine}" 51 | ), Times.Once); 52 | #pragma warning restore CC0031 // Check for null before calling a delegate 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /test/IntegrationTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("IntegrationTests")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("1d1024db-9159-42dc-9e73-b797ae7f8e4c")] 20 | -------------------------------------------------------------------------------- /test/IntegrationTests/Retry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework.Interfaces; 3 | using NUnit.Framework.Internal; 4 | using NUnit.Framework.Internal.Commands; 5 | using NUnit.Framework; 6 | using System.Threading.Tasks; 7 | using static IntegrationTests.Retrier; 8 | 9 | namespace IntegrationTests 10 | { 11 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 12 | public sealed class RetryAttribute : PropertyAttribute, IWrapSetUpTearDown 13 | { 14 | private int count; 15 | private int waitInMilliseconds; 16 | 17 | public RetryAttribute(int count = 20, int waitInMilliseconds = 500) : base(count) 18 | { 19 | this.count = count; 20 | this.waitInMilliseconds = waitInMilliseconds; 21 | } 22 | 23 | public TestCommand Wrap(TestCommand command) => new RetryCommand(command, count, waitInMilliseconds); 24 | 25 | private class RetryCommand : DelegatingTestCommand 26 | { 27 | private int retryCount; 28 | private int waitInMilliseconds; 29 | 30 | public RetryCommand(TestCommand innerCommand, int retryCount, int waitInMilliseconds) 31 | : base(innerCommand) 32 | { 33 | this.retryCount = retryCount; 34 | this.waitInMilliseconds = waitInMilliseconds; 35 | } 36 | 37 | public override TestResult Execute(TestExecutionContext context) 38 | { 39 | var count = retryCount; 40 | while (count-- > 0) 41 | { 42 | context.CurrentResult = innerCommand.Execute(context); 43 | if (context.CurrentResult.ResultState != ResultState.Failure 44 | && context.CurrentResult.ResultState != ResultState.Error) 45 | break; 46 | System.Threading.Thread.Sleep(waitInMilliseconds); 47 | } 48 | return context.CurrentResult; 49 | } 50 | } 51 | } 52 | 53 | public static class Retrier 54 | { 55 | public static void Retry(Action action, int count = 20, int waitInMilliseconds = 500) 56 | { 57 | if (count <= 1) throw new ArgumentException("Retry count needs to be larger than 1.", nameof(count)); 58 | while (true) 59 | { 60 | try 61 | { 62 | action(); 63 | break; 64 | } 65 | catch (Exception ex) when (--count > 0) 66 | { 67 | WriteMessage(ex.Message, count); 68 | System.Threading.Thread.Sleep(waitInMilliseconds); 69 | } 70 | } 71 | } 72 | 73 | public async static Task RetryAsync(Func<Task> asyncAction, int count = 20, int waitInMilliseconds = 500) 74 | { 75 | if (count <= 1) throw new ArgumentException("Retry count needs to be larger than 1.", nameof(count)); 76 | while (true) 77 | { 78 | try 79 | { 80 | await asyncAction(); 81 | break; 82 | } 83 | catch (Exception ex) when (--count > 0) 84 | { 85 | WriteMessage(ex.Message, count); 86 | await Task.Delay(waitInMilliseconds); 87 | } 88 | } 89 | } 90 | 91 | private static void WriteMessage(string message, int count) 92 | { 93 | string testName; 94 | try 95 | { 96 | testName = TestContext.CurrentContext.Test?.Name ?? "test"; 97 | } 98 | catch 99 | { 100 | testName = "test"; 101 | } 102 | Console.WriteLine($"Caught exception when running '{testName}' and will retry {count} more times. Exception message: '{message}'."); 103 | } 104 | } 105 | 106 | [TestFixture] 107 | public class TestRetryAttribute 108 | { 109 | public static int i, j; 110 | [Test, Retry(3)] 111 | public void CheckThrow() 112 | { 113 | if (++i < 2) throw new Exception("Foooo"); 114 | } 115 | [Test, Retry(3)] 116 | public void CheckAssert() 117 | { 118 | Assert.True(++j > 2); 119 | } 120 | } 121 | 122 | [TestFixture] 123 | public class TestRetryMethod 124 | { 125 | public static int i; 126 | [OneTimeSetUp] 127 | public Task OneTimeSetUp() => RetryAsync(Setup, 2); 128 | 129 | private Task Setup() 130 | { 131 | if (++i < 2) throw new Exception("Test for retry operation."); 132 | return Task.CompletedTask; 133 | } 134 | 135 | [Test] 136 | public void RetryPlaceholder() { } 137 | } 138 | } -------------------------------------------------------------------------------- /test/IntegrationTests/UninstallerTestForGenericTool.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using System.Threading.Tasks; 7 | using static IntegrationTests.Retrier; 8 | 9 | namespace IntegrationTests 10 | { 11 | [TestFixture] 12 | public class UninstallerTestForGenericTool 13 | { 14 | private const string packageName = "dotnet-foo"; 15 | private CommandDirectoryCleanup commandDirectoryCleanup; 16 | private string baseDir; 17 | private Uninstaller uninstaller; 18 | private bool uninstalled; 19 | 20 | [OneTimeSetUp] 21 | public Task OneTimeSetUp() => RetryAsync(SetupAsync); 22 | 23 | public async Task SetupAsync() 24 | { 25 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 26 | baseDir = commandDirectoryCleanup.CommandDirectory.BaseDir; 27 | var installer = new Installer(commandDirectoryCleanup.CommandDirectory); 28 | var installed = await installer.InstallAsync(packageName, null, force: false, includePreRelease: false); 29 | installed.Should().BeTrue(); 30 | uninstaller = new Uninstaller(commandDirectoryCleanup.CommandDirectory); 31 | uninstalled = await uninstaller.UninstallAsync(packageName); 32 | uninstalled.Should().BeTrue(); 33 | } 34 | 35 | [OneTimeTearDown] 36 | public void ClassCleanup() => commandDirectoryCleanup?.Dispose(); 37 | 38 | [Test] 39 | public void DeletedRedirectFile() => 40 | File.Exists(Path.Combine(baseDir, "bin", $"{packageName}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}")).Should().BeFalse(); 41 | 42 | [Test] 43 | public void DeletedPackageDirectory() => 44 | Directory.Exists(Path.Combine(baseDir, "packages", packageName)).Should().BeFalse(); 45 | } 46 | } -------------------------------------------------------------------------------- /test/IntegrationTests/UpdaterTestForGenericToolWhenDoesNotNeedUpdate.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using System; 7 | using System.Linq; 8 | using static IntegrationTests.Retrier; 9 | using System.Runtime.InteropServices; 10 | 11 | namespace IntegrationTests 12 | { 13 | [TestFixture] 14 | public class UpdaterTestForGenericToolWhenDoesNotNeedUpdate 15 | { 16 | private const string packageName = "dotnet-foo"; 17 | private CommandDirectoryCleanup commandDirectoryCleanup; 18 | private string baseDir; 19 | private DateTime lastWriteTimeForBinFile; 20 | private DateTime lastWriteTimeForPackageDir; 21 | 22 | [OneTimeSetUp] 23 | public Task OneTimeSetUp() => RetryAsync(SetupAsync); 24 | 25 | public async Task SetupAsync() 26 | { 27 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 28 | baseDir = commandDirectoryCleanup.CommandDirectory.BaseDir; 29 | var installer = new Installer(commandDirectoryCleanup.CommandDirectory); 30 | var installed = await installer.InstallAsync(packageName, null, force: false, includePreRelease: false); 31 | installed.Should().BeTrue(); 32 | GetLastWriteTimes(); 33 | var updater = new Updater(commandDirectoryCleanup.CommandDirectory); 34 | var updateResult = await updater.UpdateAsync(packageName, force: false, includePreRelease: false); 35 | updateResult.Should().Be(Updater.UpdateResult.NotNeeded); 36 | } 37 | 38 | private void GetLastWriteTimes() 39 | { 40 | lastWriteTimeForBinFile = new FileInfo(Path.Combine(baseDir, "bin", $"{packageName}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}")).LastWriteTime; 41 | var directory = commandDirectoryCleanup.CommandDirectory.GetDirectoryForPackage(packageName); 42 | var packageDir = Directory.EnumerateDirectories(directory).First(); 43 | lastWriteTimeForPackageDir = new DirectoryInfo(packageDir).LastWriteTime; 44 | } 45 | 46 | [OneTimeTearDown] 47 | public void ClassCleanup() => commandDirectoryCleanup?.Dispose(); 48 | 49 | [Test] 50 | public void DidNotUpdateRedirectFile() => 51 | new FileInfo(Path.Combine(baseDir, "bin", $"{packageName}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}")).LastWriteTime.Should().Be(lastWriteTimeForBinFile); 52 | 53 | [Test] 54 | public void DidNotUpdatePackageDir() 55 | { 56 | var directory = commandDirectoryCleanup.CommandDirectory.GetDirectoryForPackage(packageName); 57 | var packageDir = Directory.EnumerateDirectories(directory).First(); 58 | new DirectoryInfo(packageDir).LastWriteTime.Should().Be(lastWriteTimeForPackageDir); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /test/IntegrationTests/UpdaterTestForGenericToolWhenDoesNotNeedUpdateBecauseGreater.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NUnit.Framework; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using System; 7 | using System.Linq; 8 | using NuGet.Versioning; 9 | using static IntegrationTests.Retrier; 10 | using System.Runtime.InteropServices; 11 | 12 | namespace IntegrationTests 13 | { 14 | [TestFixture] 15 | public class UpdaterTestForGenericToolWhenDoesNotNeedUpdateBecauseGreater 16 | { 17 | private const string packageName = "dotnet-foo"; 18 | private CommandDirectoryCleanup commandDirectoryCleanup; 19 | private string baseDir; 20 | private DateTime lastWriteTimeForBinFile; 21 | private DateTime lastWriteTimeForPackageDir; 22 | 23 | [OneTimeSetUp] 24 | public Task OneTimeSetUp() => RetryAsync(SetupAsync); 25 | 26 | public async Task SetupAsync() 27 | { 28 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 29 | baseDir = commandDirectoryCleanup.CommandDirectory.BaseDir; 30 | var installer = new Installer(commandDirectoryCleanup.CommandDirectory); 31 | var installed = await installer.InstallAsync(packageName, null, force: false, includePreRelease: false); 32 | installed.Should().BeTrue(); 33 | MoveToLaterVersion(); 34 | GetLastWriteTimes(); 35 | var updater = new Updater(commandDirectoryCleanup.CommandDirectory); 36 | var updateResult = await updater.UpdateAsync(packageName, force: false, includePreRelease: false); 37 | updateResult.Should().Be(Updater.UpdateResult.NotNeeded); 38 | } 39 | 40 | private void MoveToLaterVersion() 41 | { 42 | var directory = commandDirectoryCleanup.CommandDirectory.GetDirectoryForPackage(packageName); 43 | var packageDir = Directory.EnumerateDirectories(directory).First(); 44 | var version = Path.GetFileName(packageDir); 45 | var semanticVersion = SemanticVersion.Parse(version); 46 | var greaterVersion = new SemanticVersion(semanticVersion.Major + 1, semanticVersion.Minor, semanticVersion.Patch, semanticVersion.ReleaseLabels, semanticVersion.Metadata).ToString(); 47 | var newPackageDir = Path.Combine(Directory.GetParent(packageDir).ToString(), greaterVersion); 48 | Directory.Move(packageDir, newPackageDir); 49 | var binFile = Path.Combine(baseDir, "bin", $"{packageName}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}"); 50 | File.WriteAllText(binFile, File.ReadAllText(binFile).Replace(version, greaterVersion)); 51 | } 52 | 53 | private void GetLastWriteTimes() 54 | { 55 | lastWriteTimeForBinFile = new FileInfo(Path.Combine(baseDir, "bin", $"{packageName}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}")).LastWriteTime; 56 | var directory = commandDirectoryCleanup.CommandDirectory.GetDirectoryForPackage(packageName); 57 | var packageDir = Directory.EnumerateDirectories(directory).First(); 58 | lastWriteTimeForPackageDir = new DirectoryInfo(packageDir).LastWriteTime; 59 | } 60 | 61 | [OneTimeTearDown] 62 | public void ClassCleanup() => commandDirectoryCleanup?.Dispose(); 63 | 64 | [Test] 65 | public void DidNotUpdateRedirectFile() => 66 | new FileInfo(Path.Combine(baseDir, "bin", $"{packageName}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}")).LastWriteTime.Should().Be(lastWriteTimeForBinFile); 67 | 68 | [Test] 69 | public void DidNotUpdatePackageDir() 70 | { 71 | var directory = commandDirectoryCleanup.CommandDirectory.GetDirectoryForPackage(packageName); 72 | var packageDir = Directory.EnumerateDirectories(directory).First(); 73 | new DirectoryInfo(packageDir).LastWriteTime.Should().Be(lastWriteTimeForPackageDir); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /test/IntegrationTests/UpdaterTestForGenericToolWhenNeedsUpdate.cs: -------------------------------------------------------------------------------- 1 | using DotNetCommands; 2 | using FluentAssertions; 3 | using NuGet.Versioning; 4 | using NUnit.Framework; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | using System.Threading.Tasks; 9 | using static IntegrationTests.Retrier; 10 | 11 | namespace IntegrationTests 12 | { 13 | [TestFixture] 14 | public class UpdaterTestForGenericToolWhenNeedsUpdate 15 | { 16 | private const string packageName = "dotnet-foo"; 17 | private CommandDirectoryCleanup commandDirectoryCleanup; 18 | private string baseDir; 19 | private string version; 20 | 21 | [OneTimeSetUp] 22 | public Task OneTimeSetUp() => RetryAsync(SetupAsync); 23 | 24 | private async Task SetupAsync() 25 | { 26 | commandDirectoryCleanup = new CommandDirectoryCleanup(); 27 | baseDir = commandDirectoryCleanup.CommandDirectory.BaseDir; 28 | var installer = new Installer(commandDirectoryCleanup.CommandDirectory); 29 | var installed = await installer.InstallAsync(packageName, null, force: false, includePreRelease: false); 30 | MoveToPreviousVersion(); 31 | installed.Should().BeTrue(); 32 | var updater = new Updater(commandDirectoryCleanup.CommandDirectory); 33 | var updateResult = await updater.UpdateAsync(packageName, force: false, includePreRelease: false); 34 | updateResult.Should().Be(Updater.UpdateResult.Success); 35 | } 36 | 37 | private void MoveToPreviousVersion() 38 | { 39 | var directory = commandDirectoryCleanup.CommandDirectory.GetDirectoryForPackage(packageName); 40 | var packageDir = Directory.EnumerateDirectories(directory).First(); 41 | version = Path.GetFileName(packageDir); 42 | var semanticVersion = SemanticVersion.Parse(version); 43 | semanticVersion.Major.Should().BeGreaterOrEqualTo(1, "If version is zero then we cannot safely run the test."); 44 | var smallerVersion = new SemanticVersion(semanticVersion.Major - 1, semanticVersion.Minor, semanticVersion.Patch + 1, semanticVersion.ReleaseLabels, semanticVersion.Metadata).ToString(); 45 | var newPackageDir = Path.Combine(Directory.GetParent(packageDir).ToString(), smallerVersion); 46 | Directory.Move(packageDir, newPackageDir); 47 | var binFile = Path.Combine(baseDir, "bin", $"{packageName}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}"); 48 | File.WriteAllText(binFile, File.ReadAllText(binFile).Replace(version, smallerVersion)); 49 | } 50 | 51 | [OneTimeTearDown] 52 | public void ClassCleanup() => commandDirectoryCleanup?.Dispose(); 53 | 54 | [Test] 55 | public void UpdatedRedirectFile() => 56 | File.ReadAllText(Path.Combine(baseDir, "bin", $"{packageName}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : "")}")).Should().Contain(version); 57 | 58 | [Test] 59 | public void DidNotCreateRuntimeConfigDevJsonFileWithCorrectConfig() => 60 | Directory.EnumerateFiles(baseDir, "*.runtimeconfig.dev.json", SearchOption.AllDirectories).Should().BeEmpty(); 61 | 62 | [Test] 63 | public void UpdatedVersion() 64 | { 65 | var directory = commandDirectoryCleanup.CommandDirectory.GetDirectoryForPackage(packageName); 66 | var packageDirectory = Directory.EnumerateDirectories(directory).Single(); 67 | Path.GetFileName(packageDirectory).Should().Be(version); 68 | } 69 | //todo install test to smaller version 70 | } 71 | } -------------------------------------------------------------------------------- /test/IntegrationTests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "testRunner": "nunit", 4 | "dependencies": { 5 | "Microsoft.NETCore.App": { 6 | "version": "1.0.0", 7 | "type": "platform" 8 | }, 9 | "dotnet-commands": { "target": "project" }, 10 | "NUnit": "3.4.1", 11 | "dotnet-test-nunit": "3.4.0-beta-2", 12 | "FluentAssertions": "4.13.0", 13 | "Moq": "4.6.38-alpha" 14 | }, 15 | "frameworks": { 16 | "netcoreapp1.0": { 17 | "imports": [ 18 | "dnxcore50", 19 | "portable-net45+win8" 20 | ] 21 | } 22 | } 23 | } --------------------------------------------------------------------------------