diff --git a/Fake.sln b/Fake.sln
index ee861128e4d..32b60763ae0 100644
--- a/Fake.sln
+++ b/Fake.sln
@@ -77,6 +77,10 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.DotNet.Xamarin", "src\
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Net.Http", "src\app\Fake.Net.Http\Fake.Net.Http.fsproj", "{D24CEE35-B6C0-4C92-AE18-E80F90B69974}"
EndProject
+Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Net.SSH", "src\app\Fake.Net.SSH\Fake.Net.SSH.fsproj", "{5B2A7546-A441-45C9-8176-2872E2A30477}"
+EndProject
+Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Net.FTP", "src\app\Fake.Net.FTP\Fake.Net.FTP.fsproj", "{18C490E3-EA3F-4DC5-87A0-1A02309F8664}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CCAC5CAB-03C8-4C11-ADBE-A0D05F6A4F18}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fake.Core.UnitTests", "src\test\Fake.Core.UnitTests\Fake.Core.UnitTests.fsproj", "{31A5759B-B562-43C0-A845-14EFA4091543}"
@@ -633,6 +637,30 @@ Global
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Release|x64.Build.0 = Release|Any CPU
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Release|x86.ActiveCfg = Release|Any CPU
{D24CEE35-B6C0-4C92-AE18-E80F90B69974}.Release|x86.Build.0 = Release|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Debug|x64.Build.0 = Debug|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Debug|x86.Build.0 = Debug|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Release|x64.ActiveCfg = Release|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Release|x64.Build.0 = Release|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Release|x86.ActiveCfg = Release|Any CPU
+ {5B2A7546-A441-45C9-8176-2872E2A30477}.Release|x86.Build.0 = Release|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|x64.Build.0 = Debug|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Debug|x86.Build.0 = Debug|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|Any CPU.Build.0 = Release|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|x64.ActiveCfg = Release|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|x64.Build.0 = Release|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|x86.ActiveCfg = Release|Any CPU
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664}.Release|x86.Build.0 = Release|Any CPU
{31A5759B-B562-43C0-A845-14EFA4091543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31A5759B-B562-43C0-A845-14EFA4091543}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31A5759B-B562-43C0-A845-14EFA4091543}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -1261,6 +1289,8 @@ Global
{4BCE4F9C-8FC2-4207-81F1-20CB07D852DC} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{13C1F95D-2FAD-4890-BF94-0AE7CF9AB2FC} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{D24CEE35-B6C0-4C92-AE18-E80F90B69974} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
+ {5B2A7546-A441-45C9-8176-2872E2A30477} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
+ {18C490E3-EA3F-4DC5-87A0-1A02309F8664} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{31A5759B-B562-43C0-A845-14EFA4091543} = {CCAC5CAB-03C8-4C11-ADBE-A0D05F6A4F18}
{D8850C67-0542-427A-ABCB-92174EA42C95} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
{8D72BED1-BC02-4B23-A631-4849BD0FD3E1} = {7BFFAE76-DEE9-417A-A79B-6A6644C4553A}
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 8f9986745c3..b3377a43072 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,12 @@
# Release Notes
+## 5.22.0 - 2022-02-14
+* ENHANCEMENT: Enhance template tests and add dotnet cli APIs for new command, thanks @yazeedobaid - https://github.com/fsprojects/FAKE/pull/2636
+* ENHANCEMENT: Porting SSH module to FAKE 5, thanks @ziadadeela - https://github.com/fsprojects/FAKE/pull/2652
+* ENHANCEMENT: Add missing report types, thanks @alcated - https://github.com/fsprojects/FAKE/pull/2654
+* ENHANCEMENT: Add known paths for more recent versions of msbuild, thanks @alcated - https://github.com/fsprojects/FAKE/pull/2655
+* ENHANCEMENT: Porting FTP module to FAKE 5, thanks @ziadadeela - https://github.com/fsprojects/FAKE/pull/2656
+
## 5.21.1 - 2022-01-30
* BUGFIX: Update logic of resolving SdkReferenceAssemblies., thanks @nojaf - https://github.com/fsprojects/FAKE/pull/2639
* BUGFIX: Fix FAKE template integration tests., thanks @yazeedobaid - https://github.com/fsprojects/FAKE/pull/2640
diff --git a/build.fsx b/build.fsx
index 4ce23bc253e..65116babe31 100644
--- a/build.fsx
+++ b/build.fsx
@@ -381,6 +381,8 @@ let dotnetAssemblyInfos =
"Fake.JavaScript.Yarn", "Running Yarn commands"
"Fake.JavaScript.TypeScript", "Running TypeScript compiler"
"Fake.Net.Http", "HTTP Client"
+ "Fake.Net.SSH", "SSH operations"
+ "Fake.Net.FTP", "FTP operations"
"Fake.netcore", "Command line tool"
"Fake.Runtime", "Core runtime features"
"Fake.Sql.DacPac", "Sql Server Data Tools DacPac operations (Obsolete: Use Fake.Sql.SqlPackage instead)"
diff --git a/help/templates/template.cshtml b/help/templates/template.cshtml
index f0d8457e773..7266bf7f586 100644
--- a/help/templates/template.cshtml
+++ b/help/templates/template.cshtml
@@ -201,7 +201,9 @@
Net
+ SSH
+ FTP
+
Sql
diff --git a/src/app/Fake.DotNet.Cli/DotNet.fs b/src/app/Fake.DotNet.Cli/DotNet.fs
index 5963bb86605..f3771334e3f 100644
--- a/src/app/Fake.DotNet.Cli/DotNet.fs
+++ b/src/app/Fake.DotNet.Cli/DotNet.fs
@@ -1697,3 +1697,155 @@ module DotNet =
nugetPush (fun _ -> param.WithPushParams { pushParams with PushTrials = pushParams.PushTrials - 1 }) nupkg
else
failwithf "dotnet nuget push failed with code %i" result.ExitCode
+
+ /// the languages supported by new command
+ type NewLanguage =
+ | FSharp
+ | CSharp
+ | VisualBasic
+
+ /// Convert the list option to string representation
+ override this.ToString() =
+ match this with
+ | FSharp -> "F#"
+ | CSharp -> "C#"
+ | VisualBasic -> "VB"
+
+ /// dotnet new command options
+ type NewOptions =
+ {
+ /// Common tool options
+ Common: Options
+ // Displays a summary of what would happen if the given command line were run if it would result in a template creation.
+ DryRun: bool
+ // Forces content to be generated even if it would change existing files.
+ Force: bool
+ // Filters templates based on language and specifies the language of the template to create.
+ Language: NewLanguage
+ // The name for the created output. If no name is specified, the name of the current directory is used.
+ Name: string option
+ // Disables checking for template package updates when instantiating a template.
+ NoUpdateCheck: bool
+ // Location to place the generated output. The default is the current directory.
+ Output: string option
+ }
+
+ /// Parameter default values.
+ static member Create() = {
+ Common = Options.Create()
+ DryRun = false
+ Force = false
+ Language = NewLanguage.FSharp
+ Name = None
+ NoUpdateCheck = false
+ Output = None
+ }
+
+ /// dotnet new --install options
+ type TemplateInstallOptions =
+ {
+ /// Common tool options
+ Common: Options
+ Install: string
+ NugetSource: string option
+ }
+
+ /// Parameter default values.
+ static member Create(packageOrSourceName) = {
+ Common = Options.Create()
+ Install = packageOrSourceName
+ NugetSource = None
+ }
+
+ /// dotnet new --install options
+ type TemplateUninstallOptions =
+ {
+ /// Common tool options
+ Common: Options
+ Uninstall: string
+ }
+
+ /// Parameter default values.
+ static member Create(packageOrSourceName) = {
+ Common = { Options.Create() with RedirectOutput = true }
+ Uninstall = packageOrSourceName
+ }
+
+ /// [omit]
+ let internal buildNewArgs (param: NewOptions) =
+ [
+ param.DryRun |> argOption "dry-run"
+ param.Force |> argOption "force"
+ argList2 "language" [param.Language.ToString()]
+ param.Name |> Option.toList |> argList2 "name"
+ param.NoUpdateCheck |> argOption "no-update-check"
+ param.Output |> Option.toList |> argList2 "output"
+ ]
+ |> List.concat
+ |> List.filter (not << String.IsNullOrEmpty)
+
+ /// [omit]
+ let internal buildTemplateInstallArgs (param: TemplateInstallOptions) =
+ [
+ argList2 "install" [param.Install]
+ param.NugetSource |> Option.toList |> argList2 "nuget-source"
+ ]
+ |> List.concat
+ |> List.filter (not << String.IsNullOrEmpty)
+
+ /// [omit]
+ let internal buildTemplateUninstallArgs (param: TemplateUninstallOptions) =
+ [
+ argList2 "uninstall" [param.Uninstall]
+ ]
+ |> List.concat
+ |> List.filter (not << String.IsNullOrEmpty)
+
+ /// Execute dotnet new command
+ /// ## Parameters
+ ///
+ /// - 'templateName' - template short name to create from
+ /// - 'setParams' - set version command parameters
+ let newFromTemplate templateName setParams =
+ use __ = Trace.traceTask "DotNet:new" "dotnet new command"
+ let param = NewOptions.Create() |> setParams
+ let args = Args.toWindowsCommandLine(buildNewArgs param)
+ let result = exec (fun _ -> param.Common) $"new {templateName}" args
+ if not result.OK then failwithf $"dotnet new failed with code %i{result.ExitCode}"
+ __.MarkSuccess()
+
+ /// Execute dotnet new --install
command to install a new template
+ /// ## Parameters
+ ///
+ /// - 'templateName' - template short name to install
+ /// - 'setParams' - set version command parameters
+ let installTemplate templateName setParams =
+ use __ = Trace.traceTask "DotNet:new" "dotnet new --install command"
+ let param = TemplateInstallOptions.Create(templateName) |> setParams
+ let args = Args.toWindowsCommandLine(buildTemplateInstallArgs param)
+ let result = exec (fun _ -> param.Common) "new" args
+ if not result.OK then failwithf $"dotnet new --install failed with code %i{result.ExitCode}"
+ __.MarkSuccess()
+
+ /// Execute dotnet new --uninstall command to uninstall a new template
+ /// ## Parameters
+ ///
+ /// - 'templateName' - template short name to uninstall
+ /// - 'setParams' - set version command parameters
+ let uninstallTemplate templateName =
+ use __ = Trace.traceTask "DotNet:new" "dotnet new --uninstall command"
+ let param = TemplateUninstallOptions.Create(templateName)
+ let args = Args.toWindowsCommandLine(buildTemplateUninstallArgs param)
+ let result = exec (fun _ -> param.Common) "new" args
+
+ // we will check if the uninstall command has returned error and message is template is not found.
+ // if that is the case, then we will just redirect output as success and change process result to
+ // exit code of zero.
+ let templateIsNotFoundToUninstall =
+ result.Results
+ |> List.exists(fun (result:ConsoleMessage) -> result.Message.Contains $"The template package '{templateName}' is not found.")
+
+ match templateIsNotFoundToUninstall with
+ | true -> ignore ""
+ | false -> failwithf $"dotnet new --uninstall failed with code %i{result.ExitCode}"
+ __.MarkSuccess()
diff --git a/src/app/Fake.DotNet.MSBuild/MSBuild.fs b/src/app/Fake.DotNet.MSBuild/MSBuild.fs
index f6655b09ae8..8edb06af529 100644
--- a/src/app/Fake.DotNet.MSBuild/MSBuild.fs
+++ b/src/app/Fake.DotNet.MSBuild/MSBuild.fs
@@ -130,6 +130,16 @@ module private MSBuildExeFromVsWhere =
module private MSBuildExe =
let knownMSBuildEntries =
[
+ { Version = "17.0"; Paths = [@"\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin"
+ @"\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin"
+ @"\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin"
+ @"\MSBuild\Current\Bin"
+ @"\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin"] }
+ { Version = "16.0"; Paths = [@"\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin"
+ @"\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin"
+ @"\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin"
+ @"\MSBuild\Current\Bin"
+ @"\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin"] }
{ Version = "15.0"; Paths = [@"\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin"
@"\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin"
@"\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin"
diff --git a/src/app/Fake.DotNet.Testing.DotCover/DotCover.fs b/src/app/Fake.DotNet.Testing.DotCover/DotCover.fs
index 4b24c1aaf1d..0ed577587aa 100644
--- a/src/app/Fake.DotNet.Testing.DotCover/DotCover.fs
+++ b/src/app/Fake.DotNet.Testing.DotCover/DotCover.fs
@@ -20,6 +20,8 @@ type ReportType =
| Json = 1
| Xml = 2
| NDependXml = 3
+ | DetailedXml = 4
+ | SummaryXml = 5
/// The dotCover parameter type for running coverage
type Params =
diff --git a/src/app/Fake.Net.FTP/AssemblyInfo.fs b/src/app/Fake.Net.FTP/AssemblyInfo.fs
new file mode 100644
index 00000000000..892247448ec
--- /dev/null
+++ b/src/app/Fake.Net.FTP/AssemblyInfo.fs
@@ -0,0 +1,19 @@
+// Auto-Generated by FAKE; do not edit
+namespace System
+open System.Reflection
+
+[]
+[]
+[]
+[]
+[]
+[]
+do ()
+
+module internal AssemblyVersionInformation =
+ let [] AssemblyTitle = "FAKE - F# Make FTP operations"
+ let [] AssemblyProduct = "FAKE - F# Make"
+ let [] AssemblyVersion = "5.22.0"
+ let [] AssemblyInformationalVersion = "5.22.0-alpha001.local.3+2022-02-07-14-27"
+ let [] AssemblyFileVersion = "5.22.0"
+ let [] AssemblyMetadata_BuildDate = "2022-02-07"
diff --git a/src/app/Fake.Net.FTP/FTP.fs b/src/app/Fake.Net.FTP/FTP.fs
new file mode 100644
index 00000000000..6d80178d78f
--- /dev/null
+++ b/src/app/Fake.Net.FTP/FTP.fs
@@ -0,0 +1,210 @@
+namespace Fake.Net
+
+open System
+open System.IO
+open System.Net
+open System.Text.RegularExpressions
+open Fake.Core
+
+[]
+/// Contains helpers which allow to upload a whole folder/specific file into a FTP Server.
+/// Uses `Passive Mode` FTP and handles all files as binary (and not ASCII).
+/// Assumes direct network connectivity to destination FTP server (not via a proxy).
+/// Does not support FTPS and SFTP.
+module FTP =
+
+ type FtpServerInfo =
+ { Server : string
+ Request : FtpWebRequest }
+
+ /// Gets a connection to the FTP server
+ let getServerInfo (serverNameIp : string) (user : string) (password : string) ftpMethod =
+ let ftpRequest = (WebRequest.Create serverNameIp) :?> FtpWebRequest
+ ftpRequest.Credentials <- NetworkCredential(user, password)
+ ftpRequest.Method <- ftpMethod
+ { Server = serverNameIp
+ Request = ftpRequest }
+
+ /// Writes given byte array into the given stream
+ let rec private writeChunkToReqStream (chunk : byte []) (requestStream : Stream) (br : BinaryReader) =
+ if chunk.Length <> 0 then
+ requestStream.Write(chunk, 0, chunk.Length)
+ writeChunkToReqStream (br.ReadBytes 1024) requestStream br
+
+ let inline private getSubstring (fromPos : int) (str : string) (toPos : int) = str.Substring(fromPos, toPos)
+ let inline private getLastSlashPosition (str : string) = str.LastIndexOf(@"/") + 1
+
+ let private charactersValidator (directoryName : string) =
+ let invalidChars = [ "<"; ">"; ":"; "\""; "/"; "\\"; "|"; "?"; "*" ]
+ not (List.exists directoryName.Contains invalidChars)
+
+ let private namesValidator (directoryName : string) =
+ let invalidNames =
+ [ "CON"; "PRN"; "AUX"; "NUL"; "COM1"; "COM2"; "COM3"; "COM4"; "COM5"; "COM6"; "COM7"; "COM8"; "COM9"; "LPT1";
+ "LPT2"; "LPT3"; "LPT4"; "LPT5"; "LPT6"; "LPT7"; "LPT8"; "LPT9" ]
+ not (List.exists (fun s -> s = directoryName.ToUpper()) invalidNames) &&
+ not (List.exists (fun s -> directoryName.ToUpper().StartsWith $"%s{s}.") invalidNames)
+
+ let private customValidator (directoryName : string) =
+ not (directoryName.EndsWith(" ")) &&
+ not (directoryName.EndsWith("."))
+
+ /// [omit]
+ ///Partial validation for folder name, based on http://msdn.microsoft.com/en-us/library/aa365247.aspx
+ let isValidDirectoryName (directoryName : string) =
+ let validators = [
+ charactersValidator
+ namesValidator
+ customValidator
+ ]
+ List.forall (fun validator -> directoryName |> validator) validators
+
+ /// Checks to see if the `ftp content` string contains the string `Given_Folder_Name`
+ let inline regexCheck folderName ftpContents = Regex.IsMatch(ftpContents, $@"\s+%s{folderName}\s+")
+
+ /// Gets the contents/listing of files and folders in a given FTP server folder
+ /// ## Parameters
+ /// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
+ /// - `user` - FTP Server login name (ex: "joebloggs")
+ /// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
+ /// - `dirPath` - The full name of folder whose content need to be listed
+ let getFtpDirContents (server : string) (user : string) (pwd : string) (dirPath : string) =
+ Trace.logfn $"Getting FTP dir contents for %s{dirPath}"
+ dirPath
+ |> fun d -> getServerInfo $"%s{server}/%s{d}" user pwd WebRequestMethods.Ftp.ListDirectoryDetails
+ |> fun si ->
+ use response = (si.Request.GetResponse() :?> FtpWebResponse)
+ use responseStream = response.GetResponseStream()
+ use reader = new StreamReader(responseStream)
+ reader.ReadToEnd()
+
+ /// Uploads a single file from local directory into remote FTP folder.
+ /// ## Parameters
+ /// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
+ /// - `user` - FTP Server login name (ex: "joebloggs")
+ /// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
+ /// - `destPath` - The full local file path that needs to be uploaded
+ /// - `srcPath` - The full path to file which needs to be created, including all its parent folders
+ let uploadAFile (server : string) (user : string) (pwd : string) (destPath : string) (srcPath : string) =
+ Trace.logfn $"Uploading %s{srcPath} to %s{destPath}"
+ let fl = FileInfo(srcPath)
+ if (fl.Length <> 0L) then
+ destPath
+ |> fun d -> getServerInfo $"%s{server}/%s{d}" user pwd WebRequestMethods.Ftp.UploadFile
+ |> fun si ->
+ use fs = new FileStream(srcPath, FileMode.Open, FileAccess.Read)
+ use br = new BinaryReader(fs, System.Text.UTF8Encoding())
+ use requestStream = si.Request.GetRequestStream()
+ writeChunkToReqStream (br.ReadBytes 1024) requestStream br
+
+ /// Given a folder name, will check if that folder is present at a given root directory of a FTP server.
+ /// ## Parameters
+ /// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
+ /// - `user` - FTP Server login name (ex: "joebloggs")
+ /// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
+ let private isFolderInDirectoryList server user pwd destPath folderName =
+ destPath
+ |> getLastSlashPosition
+ |> getSubstring 0 destPath
+ |> getFtpDirContents server user pwd
+ |> regexCheck folderName
+
+ /// Given a folder path, will check if that folder is present at a given root directory of a FTP server.
+ /// ## Parameters
+ /// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
+ /// - `user` - FTP Server login name (ex: "joebloggs")
+ /// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
+ /// - `destPath` - The full name of folder which needs to be checked for existence, including all its parent folders
+ let isFolderPresent server user pwd (destPath : string) =
+ destPath
+ |> getLastSlashPosition
+ |> destPath.Substring
+ |> isFolderInDirectoryList server user pwd destPath
+
+ /// Creates a matching folder in FTP folder, if not already present.
+ /// ## Parameters
+ /// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
+ /// - `user` - FTP Server login name (ex: "joebloggs")
+ /// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
+ /// - `destPath` - The full name of folder which needs to be created, including all its parent folders
+ let createAFolder (server : string) (user : string) (pwd : string) (destPath : string) =
+ Trace.logfn $"Creating folder %s{destPath}"
+ if not ((String.IsNullOrEmpty destPath) || (isFolderPresent server user pwd destPath)) then
+ destPath
+ |> fun d -> getServerInfo $"%s{server}/%s{d}" user pwd WebRequestMethods.Ftp.MakeDirectory
+ |> fun si ->
+ use response = (si.Request.GetResponse() :?> FtpWebResponse)
+ Trace.logfn $"Create folder status: %s{response.StatusDescription}"
+
+ /// Uploads a given local folder to a given root dir on a FTP server.
+ /// ## Parameters
+ /// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
+ /// - `user` - FTP Server login name (ex: "joebloggs")
+ /// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
+ /// - `srcPath` - The local server path from which files need to be uploaded
+ /// - `rootDir` - The remote root dir where files need to be uploaded, leave this as empty, if files need to be uploaded to root dir of FTP server
+ let rec uploadAFolder server user pwd (srcPath : string) (rootDir : string) =
+ Trace.logfn $"Uploading folder %s{srcPath}"
+ let dirInfo = DirectoryInfo(srcPath)
+ if dirInfo.Exists && isValidDirectoryName rootDir then
+ dirInfo.GetFileSystemInfos() |> Seq.iter (fun fsi -> upload server user pwd fsi rootDir)
+
+ and private upload server user pwd (fsi : FileSystemInfo) (rootDir : string) =
+ match fsi.GetType().ToString() with
+ | "System.IO.DirectoryInfo" ->
+ createAFolder server user pwd rootDir
+ createAFolder server user pwd $"%s{rootDir}/%s{fsi.Name}"
+ uploadAFolder server user pwd fsi.FullName $"%s{rootDir}/%s{fsi.Name}"
+ | "System.IO.FileInfo" -> uploadAFile server user pwd $"%s{rootDir}/%s{fsi.Name}" fsi.FullName
+ | _ -> Trace.logfn $"Unknown object found at %A{fsi}"
+
+ /// Deletes a single file from remote FTP folder.
+ /// ## Parameters
+ /// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
+ /// - `user` - FTP Server login name (ex: "joebloggs")
+ /// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
+ /// - `destPath` - The full path to the file which needs to be deleted, including all its parent folders
+ let deleteAFile (server : string) (user : string) (pwd : string) (destPath : string) =
+ Trace.logfn $"Deleting %s{destPath}"
+ destPath
+ |> fun p -> getServerInfo $"%s{server}/%s{p}" user pwd WebRequestMethods.Ftp.DeleteFile
+ |> fun si ->
+ use response = (si.Request.GetResponse() :?> FtpWebResponse)
+ Trace.logfn $"Delete file %s{destPath} status: %s{response.StatusDescription}"
+
+ let private getFolderContents (server : string) (user : string) (pwd : string) (destPath : string) =
+ getServerInfo $"%s{server}/%s{destPath}" user pwd WebRequestMethods.Ftp.ListDirectory
+ |> fun si ->
+ use response = (si.Request.GetResponse() :?> FtpWebResponse)
+ use responseStream = response.GetResponseStream()
+ use reader = new StreamReader(responseStream)
+ [ while not reader.EndOfStream do yield reader.ReadLine() ]
+
+ let private deleteEmptyFolder (server : string) (user : string) (pwd : string) (destPath : string) =
+ destPath
+ |> fun p -> getServerInfo $"%s{server}/%s{p}" user pwd WebRequestMethods.Ftp.RemoveDirectory
+ |> fun si ->
+ use response = (si.Request.GetResponse() :?> FtpWebResponse)
+ Trace.logfn $"Delete folder %s{destPath} status: %s{response.StatusDescription}"
+
+ /// Deletes a single folder from remote FTP folder.
+ /// ## Parameters
+ /// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
+ /// - `user` - FTP Server login name (ex: "joebloggs")
+ /// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
+ /// - `destPath` - The full path to the folder which needs to be deleted, including all its parent folders
+ let rec deleteAFolder (server : string) (user : string) (pwd : string) (destPath : string) =
+ Trace.logfn $"Deleting %s{destPath}"
+ let folderContents = getFolderContents server user pwd destPath
+
+ if folderContents |> List.isEmpty then
+ deleteEmptyFolder server user pwd destPath
+ else
+ folderContents
+ |> List.iter (fun entry ->
+ try
+ deleteAFile server user pwd (Path.Combine(destPath, entry))
+ with
+ | _ -> deleteAFolder server user pwd (Path.Combine(destPath, entry)))
+
+ deleteEmptyFolder server user pwd destPath
diff --git a/src/app/Fake.Net.FTP/Fake.Net.FTP.fsproj b/src/app/Fake.Net.FTP/Fake.Net.FTP.fsproj
new file mode 100644
index 00000000000..faf7f932b75
--- /dev/null
+++ b/src/app/Fake.Net.FTP/Fake.Net.FTP.fsproj
@@ -0,0 +1,21 @@
+
+
+ netstandard2.0;net472
+ Fake.Net.FTP
+ Library
+
+
+ $(DefineConstants)
+
+
+ $(DefineConstants);RELEASE
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/Fake.Net.FTP/paket.references b/src/app/Fake.Net.FTP/paket.references
new file mode 100644
index 00000000000..edc34b53625
--- /dev/null
+++ b/src/app/Fake.Net.FTP/paket.references
@@ -0,0 +1,4 @@
+group netcore
+
+FSharp.Core
+NETStandard.Library
diff --git a/src/app/Fake.Net.SSH/AssemblyInfo.fs b/src/app/Fake.Net.SSH/AssemblyInfo.fs
new file mode 100644
index 00000000000..1b35e8b65aa
--- /dev/null
+++ b/src/app/Fake.Net.SSH/AssemblyInfo.fs
@@ -0,0 +1,19 @@
+// Auto-Generated by FAKE; do not edit
+namespace System
+open System.Reflection
+
+[]
+[]
+[]
+[]
+[]
+[]
+do ()
+
+module internal AssemblyVersionInformation =
+ let [] AssemblyTitle = "FAKE - F# Make SSH operations"
+ let [] AssemblyProduct = "FAKE - F# Make"
+ let [] AssemblyVersion = "5.21.1"
+ let [] AssemblyInformationalVersion = "5.21.1"
+ let [] AssemblyFileVersion = "5.21.1"
+ let [] AssemblyMetadata_BuildDate = "2022-01-31"
diff --git a/src/app/Fake.Net.SSH/Fake.Net.SSH.fsproj b/src/app/Fake.Net.SSH/Fake.Net.SSH.fsproj
new file mode 100644
index 00000000000..2d7e47e2d1b
--- /dev/null
+++ b/src/app/Fake.Net.SSH/Fake.Net.SSH.fsproj
@@ -0,0 +1,23 @@
+
+
+ netstandard2.0;net472
+ Fake.Net.SSH
+ Library
+
+
+ $(DefineConstants)
+
+
+ $(DefineConstants);RELEASE
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/Fake.Net.SSH/SSH.fs b/src/app/Fake.Net.SSH/SSH.fs
new file mode 100644
index 00000000000..c71cf2dd64f
--- /dev/null
+++ b/src/app/Fake.Net.SSH/SSH.fs
@@ -0,0 +1,63 @@
+namespace Fake.Net
+
+open System
+open Fake.Core
+
+[]
+/// Contains a task which allows to perform SSH operations
+module SSH =
+
+ /// The SSH parameter type.
+ type SSHParams =
+ { /// Path of the scp.exe
+ ToolPath : string
+ /// Path of the private key file (optional)
+ PrivateKeyPath : string
+ /// remote User
+ RemoteUser : string
+ RemoteHost : string
+ RemotePort : string
+ }
+
+ /// The SSH default parameters
+ let SSHDefaults : SSHParams =
+ { ToolPath = if Environment.isMono then "ssh" else "ssh.exe"
+ RemoteUser = "fake"
+ RemoteHost = "localhost"
+ RemotePort = "22"
+ PrivateKeyPath = null
+ }
+
+ let private getTarget sshParams =
+ match sshParams.RemotePort with
+ | "22" -> $"%s{sshParams.RemoteUser}@%s{sshParams.RemoteHost}"
+ | _ -> $"%s{sshParams.RemoteUser}@%s{sshParams.RemoteHost}:%s{sshParams.RemotePort}"
+
+ let private getPrivateKey privateKeyPath =
+ if String.IsNullOrEmpty privateKeyPath then "" else $"-i \"%s{privateKeyPath}\""
+
+ let buildArguments sshParams command =
+ let target = sshParams |> getTarget
+ let privateKey = sshParams.PrivateKeyPath |> getPrivateKey
+ $"%s{privateKey} %s{target} %s{Args.toWindowsCommandLine [command]}" |> String.trim
+
+ /// Performs a command via SSH.
+ /// ## Parameters
+ ///
+ /// - `setParams` - Function used to manipulate the default SSHParams value.
+ /// - `command` - The target path. Can be something like user@host:directory/TargetFile or a local path.
+ ///
+ /// ## Sample
+ ///
+ /// SSH (fun p -> { p with ToolPath = "tools/ssh.exe" }) command
+ let SSH setParams command =
+ let (sshParams : SSHParams) = setParams SSHDefaults
+ let target = sshParams |> getTarget
+ let args = buildArguments sshParams command
+
+ Trace.tracefn $"%s{sshParams.ToolPath} %s{args}"
+
+ let result = CreateProcess.fromRawCommandLine sshParams.ToolPath args
+ |> CreateProcess.withTimeout(TimeSpan.MaxValue)
+ |> Proc.run
+ if result.ExitCode <> 0 then failwithf $"Error during SSH. Target: %s{target} Command: %s{command}"
diff --git a/src/app/Fake.Net.SSH/paket.references b/src/app/Fake.Net.SSH/paket.references
new file mode 100644
index 00000000000..edc34b53625
--- /dev/null
+++ b/src/app/Fake.Net.SSH/paket.references
@@ -0,0 +1,4 @@
+group netcore
+
+FSharp.Core
+NETStandard.Library
diff --git a/src/legacy/FakeLib/FakeLib.fsproj b/src/legacy/FakeLib/FakeLib.fsproj
index 7df7ce5abbb..9998647cc8d 100644
--- a/src/legacy/FakeLib/FakeLib.fsproj
+++ b/src/legacy/FakeLib/FakeLib.fsproj
@@ -248,6 +248,12 @@
Fake.Net.Http/Http.fs
+
+ Fake.Net.SSH/SSH.fs
+
+
+ Fake.Net.FTP/FTP.fs
+
Fake.Core.Xml/Xml.fs
diff --git a/src/legacy/FakeLib/FtpHelper.fs b/src/legacy/FakeLib/FtpHelper.fs
index e6c205028a9..c28623c9115 100644
--- a/src/legacy/FakeLib/FtpHelper.fs
+++ b/src/legacy/FakeLib/FtpHelper.fs
@@ -2,7 +2,7 @@
/// Uses `Passive Mode` FTP and handles all files as binary (and not ASCII).
/// Assumes direct network connectivity to destination FTP server (not via a proxy).
/// Does not support FTPS and SFTP.
-[]
+[]
module Fake.FtpHelper
open System
@@ -10,13 +10,13 @@ open System.IO
open System.Net
open System.Text.RegularExpressions
-[]
+[]
type FtpServerInfo =
{ Server : string
Request : FtpWebRequest }
/// Gets a connection to the FTP server
-[]
+[]
let getServerInfo (serverNameIp : string) (user : string) (password : string) ftpMethod =
let ftpRequest = (WebRequest.Create serverNameIp) :?> FtpWebRequest
ftpRequest.Credentials <- new NetworkCredential(user, password)
@@ -47,7 +47,7 @@ let inline private dirNameIsValid (dirName : string) =
not (invalid1 && invalid2 && invalid3 && invalid4 && invalid5)
/// Checks to see if the `ftp content` string containts the string `Given_Folder_Name`
-[]
+[]
let inline regexCheck fname ftpContents = Regex.IsMatch(ftpContents, (sprintf @"\s+%s\s+" fname))
/// Gets the contents/listing of files and folders in a given FTP server folder
@@ -56,7 +56,7 @@ let inline regexCheck fname ftpContents = Regex.IsMatch(ftpContents, (sprintf @"
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
-[]
+[]
let getFtpDirContents (server : string) (user : string) (pwd : string) (dirPath : string) =
logfn "Getting FTP dir contents for %s" dirPath
dirPath
@@ -74,7 +74,7 @@ let getFtpDirContents (server : string) (user : string) (pwd : string) (dirPath
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
-[]
+[]
let uploadAFile (server : string) (user : string) (pwd : string) (destPath : string) (srcPath : string) =
logfn "Uploading %s to %s" srcPath destPath
let fl = new FileInfo(srcPath)
@@ -105,7 +105,7 @@ let private checkInExistingDirList server user pwd destPath fname =
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
-[]
+[]
let isFolderPresent server user pwd (destPath : string) =
destPath
|> lastSlashPos
@@ -118,7 +118,7 @@ let isFolderPresent server user pwd (destPath : string) =
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
-[]
+[]
let createAFolder (server : string) (user : string) (pwd : string) (destPath : string) =
logfn "Creating folder %s" destPath
if not ((String.IsNullOrEmpty destPath) || (isFolderPresent server user pwd destPath)) then
@@ -135,7 +135,7 @@ let createAFolder (server : string) (user : string) (pwd : string) (destPath : s
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
-[]
+[]
let rec uploadAFolder server user pwd (srcPath : string) (rootDir : string) =
logfn "Uploading folder %s" srcPath
let dirInfo = new DirectoryInfo(srcPath)
@@ -157,7 +157,7 @@ and private upload server user pwd (fsi : FileSystemInfo) (rootDir : string) =
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
-[]
+[]
let deleteAFile (server : string) (user : string) (pwd : string) (destPath : string) =
logfn "Deleting %s" destPath
destPath
@@ -187,7 +187,7 @@ let private deleteEmptyFolder (server : string) (user : string) (pwd : string) (
/// - `server` - FTP Server name (ex: "ftp://10.100.200.300:21/")
/// - `user` - FTP Server login name (ex: "joebloggs")
/// - `pwd` - FTP Server login password (ex: "J0Eblogg5")
-[]
+[]
let rec deleteAFolder (server : string) (user : string) (pwd : string) (destPath : string) =
logfn "Deleting %s" destPath
let folderContents = getFolderContents server user pwd destPath
diff --git a/src/legacy/FakeLib/SSHHelper.fs b/src/legacy/FakeLib/SSHHelper.fs
index 46c43f0ba81..b30f4af9175 100644
--- a/src/legacy/FakeLib/SSHHelper.fs
+++ b/src/legacy/FakeLib/SSHHelper.fs
@@ -1,11 +1,11 @@
[]
-[]
+[]
/// Conatins a task which allows to perform SSH operations
module Fake.SSHHelper
/// The SSH parameter type.
[]
-[]
+[]
type SSHParams =
{ /// Path of the scp.exe
ToolPath : string
@@ -19,7 +19,7 @@ type SSHParams =
/// The SSH default parameters
-[]
+[]
let SSHDefaults : SSHParams =
{ ToolPath = if isMono then "ssh" else "ssh.exe"
RemoteUser = "fake"
@@ -37,7 +37,7 @@ let SSHDefaults : SSHParams =
/// ## Sample
///
/// SSH (fun p -> { p with ToolPath = "tools/ssh.exe" }) command
-[]
+[]
let SSH setParams command =
let (p : SSHParams) = setParams SSHDefaults
let target =
diff --git a/src/test/Fake.Core.UnitTests/Fake.Core.UnitTests.fsproj b/src/test/Fake.Core.UnitTests/Fake.Core.UnitTests.fsproj
index d917134135c..5af1f99f104 100644
--- a/src/test/Fake.Core.UnitTests/Fake.Core.UnitTests.fsproj
+++ b/src/test/Fake.Core.UnitTests/Fake.Core.UnitTests.fsproj
@@ -15,6 +15,8 @@
+
+
@@ -76,6 +78,8 @@
+
+
diff --git a/src/test/Fake.Core.UnitTests/Fake.DotNet.Cli.fs b/src/test/Fake.Core.UnitTests/Fake.DotNet.Cli.fs
index aeeaeb6656a..32e28fadb91 100644
--- a/src/test/Fake.Core.UnitTests/Fake.DotNet.Cli.fs
+++ b/src/test/Fake.Core.UnitTests/Fake.DotNet.Cli.fs
@@ -82,4 +82,64 @@ let tests =
let expected = "--configuration Release --manifest Path1 --manifest Path2"
Expect.equal cli expected "Push args generated correctly."
+
+ testCase "Test that the dotnet new command works as expected" <| fun _ ->
+ let param =
+ { DotNet.NewOptions.Create() with
+ DryRun = true
+ Force = true
+ Language = DotNet.NewLanguage.FSharp
+ Name = Some("my-awesome-project")
+ NoUpdateCheck = true
+ Output = Some("/path/to/code") }
+ let cli =
+ param
+ |> DotNet.buildNewArgs
+ |> Args.toWindowsCommandLine
+
+ let expected = "--dry-run --force --language F# --name my-awesome-project --no-update-check --output /path/to/code"
+
+ Expect.equal cli expected "New args generated correctly."
+
+ testCase "Test that the dotnet new command works as expected with spaces in arguments" <| fun _ ->
+ let param =
+ { DotNet.NewOptions.Create() with
+ DryRun = true
+ Force = true
+ Language = DotNet.NewLanguage.FSharp
+ Name = Some("my awesome project")
+ NoUpdateCheck = true
+ Output = Some("/path to/code") }
+ let cli =
+ param
+ |> DotNet.buildNewArgs
+ |> Args.toWindowsCommandLine
+
+ let expected = "--dry-run --force --language F# --name \"my awesome project\" --no-update-check --output \"/path to/code\""
+
+ Expect.equal cli expected "New args generated correctly."
+
+ testCase "Test that the dotnet new --install command works as expected" <| fun _ ->
+ let param =
+ { DotNet.TemplateInstallOptions.Create("my-awesome-template") with
+ NugetSource = Some("C:\\path\\to\\tool") }
+ let cli =
+ param
+ |> DotNet.buildTemplateInstallArgs
+ |> Args.toWindowsCommandLine
+
+ let expected = "--install my-awesome-template --nuget-source \"C:\\path\\to\\tool\""
+
+ Expect.equal cli expected "New --install args generated correctly."
+
+ testCase "Test that the dotnet new --uninstall command works as expected" <| fun _ ->
+ let param = DotNet.TemplateUninstallOptions.Create("my-awesome-template")
+ let cli =
+ param
+ |> DotNet.buildTemplateUninstallArgs
+ |> Args.toWindowsCommandLine
+
+ let expected = "--uninstall my-awesome-template"
+
+ Expect.equal cli expected "New --uninstall args generated correctly."
]
diff --git a/src/test/Fake.Core.UnitTests/Fake.Net.FTP.fs b/src/test/Fake.Core.UnitTests/Fake.Net.FTP.fs
new file mode 100644
index 00000000000..d144ce9ab5b
--- /dev/null
+++ b/src/test/Fake.Core.UnitTests/Fake.Net.FTP.fs
@@ -0,0 +1,31 @@
+module Fake.Net.FTPTests
+
+open Expecto
+open Fake.Net
+
+[]
+let tests =
+ let directoryNameTestCases =
+ [ "invalidname", false
+ "invalid:name", false
+ "invalid/name", false
+ "invalid\\name", false
+ "invalid\"name", false
+ "invalid|name", false
+ "invalid?name", false
+ "invalid*name", false
+ "invalidname ", false
+ "invalidname.", false
+ "CON", false
+ "LPT4", false
+ "nul", false
+ "valid-name", true]
+ |> List.map (fun (directoryName, expected) ->
+ testCase $"Validate directory name '%s{directoryName}'" <| fun _ ->
+ let isValid = FTP.isValidDirectoryName directoryName
+ Expect.equal isValid expected "expected proper directory name validation")
+
+ testList "Fake.Net.FTP.Tests" [
+ yield! directoryNameTestCases
+ ]
diff --git a/src/test/Fake.Core.UnitTests/Fake.Net.SSH.fs b/src/test/Fake.Core.UnitTests/Fake.Net.SSH.fs
new file mode 100644
index 00000000000..95015d3db22
--- /dev/null
+++ b/src/test/Fake.Core.UnitTests/Fake.Net.SSH.fs
@@ -0,0 +1,44 @@
+module Fake.Net.SSHTests
+
+open Expecto
+open Fake.Net
+
+[]
+let tests =
+ testList "Fake.Net.SSH.Tests" [
+ testCase "Test all arguments are mapped correctly" <| fun _ ->
+ let args: SSH.SSHParams =
+ { ToolPath = "ssh"
+ RemoteUser = "fake-user"
+ RemoteHost = "localhost"
+ RemotePort = "22"
+ PrivateKeyPath = "private-key-path" }
+ let sshCommand = "pwd"
+ let cmd = SSH.buildArguments args sshCommand
+
+ Expect.equal cmd "-i \"private-key-path\" fake-user@localhost pwd" "expected proper arguments formatting"
+
+ testCase "Test ssh target is mapped correctly when a custom port is used" <| fun _ ->
+ let args: SSH.SSHParams =
+ { ToolPath = "ssh"
+ RemoteUser = "fake-user"
+ RemoteHost = "localhost"
+ RemotePort = "2222"
+ PrivateKeyPath = null }
+ let sshCommand = "pwd"
+ let cmd = SSH.buildArguments args sshCommand
+
+ Expect.equal cmd "fake-user@localhost:2222 pwd" "expected proper arguments formatting"
+
+ testCase "Test PrivateKeyPath is mapped correctly when it's empty" <| fun _ ->
+ let args: SSH.SSHParams =
+ { ToolPath = "ssh"
+ RemoteUser = "fake-user"
+ RemoteHost = "localhost"
+ RemotePort = "22"
+ PrivateKeyPath = null }
+ let sshCommand = "pwd"
+ let cmd = SSH.buildArguments args sshCommand
+
+ Expect.equal cmd "fake-user@localhost pwd" "expected proper arguments formatting"
+ ]
diff --git a/src/test/Fake.DotNet.Cli.IntegrationTests/TemplateTests.fs b/src/test/Fake.DotNet.Cli.IntegrationTests/TemplateTests.fs
index a83cc9dba92..7e14373f9cb 100644
--- a/src/test/Fake.DotNet.Cli.IntegrationTests/TemplateTests.fs
+++ b/src/test/Fake.DotNet.Cli.IntegrationTests/TemplateTests.fs
@@ -2,7 +2,6 @@
open Expecto
open System
-open System.Linq
open System.IO
open Fake.Core
@@ -13,7 +12,23 @@ let templateProj = "fake-template.fsproj"
let templatePackageName = "fake-template"
let templateName = "fake"
-//TODO: add DotNetCli helpers for the `new` command
+type BootstrapKind =
+ | Tool
+ | Local
+ | None
+ with override x.ToString () = match x with | Tool -> "tool" | Local -> "local" | None -> "none"
+
+type DslKind =
+ | Fake
+ | BuildTask
+ with override x.ToString () = match x with | Fake -> "fake" | BuildTask -> "buildtask"
+
+type DependenciesKind =
+ | File
+ | Inline
+ | None
+ with override x.ToString () = match x with | File -> "file" | Inline -> "inline" | None -> "none"
+
let dotnetSdk = lazy DotNet.install DotNet.Versions.FromGlobalJson
@@ -22,109 +37,107 @@ let inline opts () = DotNet.Options.lift dotnetSdk.Value
let inline dtntWorkDir wd =
DotNet.Options.lift dotnetSdk.Value
>> DotNet.Options.withWorkingDirectory wd
-
+
let inline redirect () =
DotNet.Options.lift (fun opts -> { opts with RedirectOutput = true })
-let uninstallTemplate () =
- let result = DotNet.exec (opts() >> redirect()) "new" $"-u %s{templatePackageName}"
-
- // we will check if the install command has returned error and message is template is not found.
- // if that is the case, then we will just redirect output as success and change process result to
- // exit code of zero.
- match result.Results.Any(fun (result:ConsoleMessage) -> result.Message.Equals $"The template package '{templatePackageName}' is not found.") with
- | true -> ProcessResult.New 0 result.Results
- | false -> result
-
-let installTemplateFrom pathToNupkg =
- DotNet.exec (opts() >> redirect()) "new" (sprintf "-i %s" pathToNupkg)
+let getDebuggingInfo() =
+ sprintf "%s\nDOTNET_ROOT: %s\nPATH: %s\n" (Environment.GetEnvironmentVariable("DOTNET_ROOT")) (Environment.GetEnvironmentVariable "PATH")
-type BootstrapKind =
-| Tool
-| Local
-| None
-with override x.ToString () = match x with | Tool -> "tool" | Local -> "local" | None -> "none"
-
-type DslKind =
-| Fake
-| BuildTask
-with override x.ToString () = match x with | Fake -> "fake" | BuildTask -> "buildtask"
+let isProcessSucceeded message (r: ProcessResult) =
+ $"Message: {message}\n
+ Exit Code: {r.ExitCode}\n
+ Debugging Info: {getDebuggingInfo}\n
+ Result:\n stderr: {r.Result.Error}\n stdout: {r.Result.Output}"
+ |> Expect.isTrue (r.ExitCode = 0)
-type DependenciesKind =
-| File
-| Inline
-| None
-with override x.ToString () = match x with | File -> "file" | Inline -> "inline" | None -> "none"
-
-let shouldSucceed message (r: ProcessResult) =
- let errorStr =
- r.Results
- |> Seq.map (fun r -> sprintf "%s: %s" (if r.IsError then "stderr" else "stdout") r.Message)
- |> fun s -> String.Join("\n", s)
- Expect.isTrue
- r.OK
- (sprintf
- "%s. Exit code '%d'.\nDOTNET_ROOT: %s\nPATH: %s\n Results:\n%s\n"
- message r.ExitCode (Environment.GetEnvironmentVariable("DOTNET_ROOT"))
- (Environment.GetEnvironmentVariable "PATH") errorStr)
-
-let timeout = (System.TimeSpan.FromMinutes 10.)
+let timeout = (TimeSpan.FromMinutes 10.)
let runTemplate rootDir kind dependencies dsl =
Directory.ensure rootDir
try
- DotNet.exec (dtntWorkDir rootDir >> redirect()) "new" (sprintf "%s --allow-scripts yes --bootstrap %s --dependencies %s --dsl %s" templateName (string kind) (string dependencies) (string dsl))
- |> shouldSucceed "should have run the template successfully"
+ let result =
+ DotNet.exec (dtntWorkDir rootDir >> redirect()) "new" $"{templateName} --allow-scripts yes --bootstrap {string kind} --dependencies {string dependencies} --dsl {string dsl}"
+
+ let errors =
+ result.Results
+ |> List.filter(fun res -> res.IsError)
+ |> List.map(fun res -> res.Message)
+
+ let messages =
+ result.Results
+ |> List.filter(fun res -> not res.IsError)
+ |> List.map(fun res -> res.Message)
+
+ let processResult: ProcessResult = {
+ ExitCode = result.ExitCode
+ Result = {Output = String.Join ("\n", messages); Error = String.Join ("\n", errors)}
+ }
+
+ isProcessSucceeded "should have run the template successfully" processResult
with e ->
if e.Message.Contains "Command succeeded" &&
e.Message.Contains "was created successfully" then
- printfn "Ignoring exit-code while template creation: %O" e
+ printfn $"Ignoring exit-code while template creation: {e}"
else reraise()
-
-let invokeScript dir scriptName args =
+let invokeScript dir scriptName (args: string) =
let fullScriptPath = Path.Combine(dir, scriptName)
-
- Process.execWithResult
- (fun x ->
- x.WithWorkingDirectory(dir)
- .WithFileName(fullScriptPath)
- .WithArguments args) timeout
+ CreateProcess.fromRawCommandLine fullScriptPath args
+ |> CreateProcess.withTimeout timeout
+ |> CreateProcess.withWorkingDirectory dir
+ |> CreateProcess.redirectOutput
+ |> Proc.run
let fileContainsText dir fileName text =
let filePath = Path.Combine(dir, fileName)
let content = File.ReadAllText(filePath)
content.Contains(text: string)
-let expectMissingTarget targetName (r: ProcessResult) =
- let contains = r.Errors |> Seq.exists (fun err -> err.Contains (sprintf "Target \"%s\" is not defined" targetName))
- Expect.isTrue contains (sprintf "Expected the message 'Target %%s is not defined' but got: %s" (String.Join("\n", r.Errors)))
+let expectMissingTarget targetName (r: ProcessResult) =
+ let contains = r.Result.Error.Contains $"Target \"{targetName}\" is not defined"
+ Expect.isTrue contains $"Expected the message 'Target {targetName} is not defined' but got: {r.Result.Error}"
let tempDir() = Path.Combine("../../../test/fake-template", Path.GetRandomFileName())
let fileExists dir fileName = File.Exists(Path.Combine(dir, fileName))
+let setupTemplate() =
+ Process.setEnableProcessTracing true
+
+ try
+ DotNet.uninstallTemplate templatePackageName
+ with exn ->
+ $"should clear out preexisting templates\nDebugging Info: {getDebuggingInfo}"
+ |> Expect.isTrue false
+
+ printfn $"%s{Environment.CurrentDirectory}"
+
+ DotNet.setupEnv dotnetSdk.Value
+
+ let templateNupkg =
+ GlobbingPattern.create "../../../release/dotnetcore/fake-template.*.nupkg"
+ |> GlobbingPattern.setBaseDir __SOURCE_DIRECTORY__
+ |> Seq.toList
+ |> List.rev
+ |> List.tryHead
+
+ let fakeTemplateName =
+ match templateNupkg with
+ | Some t -> t
+ | Option.None -> templatePackageName
+ try
+ DotNet.installTemplate fakeTemplateName id
+ with exn ->
+ $"should install new FAKE template\nDebugging Info: {getDebuggingInfo}"
+ |> Expect.isTrue false
+
[]
let tests =
// we need to (uninstall) the template, install the packed version, and then execute that template
testList "Fake.DotNet.Cli.IntegrationTests.Template tests" [
testList "can install and run the template" [
- Process.setEnableProcessTracing true
- uninstallTemplate () |> shouldSucceed "should clear out preexisting templates"
- printfn "%s" Environment.CurrentDirectory
-
- DotNet.setupEnv dotnetSdk.Value
- let templateNupkg =
- GlobbingPattern.create "../../../release/dotnetcore/fake-template.*.nupkg"
- |> GlobbingPattern.setBaseDir __SOURCE_DIRECTORY__
- |> Seq.toList
- |> List.rev
- |> List.tryHead
- let installArgument =
- match templateNupkg with
- | Some t -> t
- | Option.None -> "fake-template"
- installTemplateFrom installArgument |> shouldSucceed "should install new FAKE template"
+ setupTemplate()
let scriptFile =
if Environment.isUnix
@@ -139,7 +152,7 @@ let tests =
runTemplate tempDir Tool File Fake
Expect.isFalse (Directory.Exists (Path.Combine(tempDir, ".fake"))) "After creating the template the '.fake' directory should not exist!"
let result = invokeScript tempDir scriptFile "build -t Nonexistent"
- Expect.isFalse result.OK "the script should have failed"
+ Expect.isFalse (result.ExitCode = 0) "the script should have failed"
expectMissingTarget "Nonexistent" result
}
@@ -166,7 +179,8 @@ let tests =
runTemplate tempDir Tool File BuildTask
Expect.isFalse (Directory.Exists (Path.Combine(tempDir, ".fake"))) "After creating the template the '.fake' directory should not exist!"
- invokeScript tempDir scriptFile "build -t All" |> shouldSucceed "should build successfully"
+ invokeScript tempDir scriptFile "build -t All"
+ |> isProcessSucceeded "should build successfully"
}
yield test "can install a buildtask-dsl inline-dependencies template" {
@@ -182,15 +196,14 @@ let tests =
runTemplate tempDir Tool Inline BuildTask
Expect.isFalse (Directory.Exists (Path.Combine(tempDir, ".fake"))) "After creating the template the '.fake' directory should not exist!"
- invokeScript tempDir scriptFile "build -t All" |> shouldSucceed "should build successfully"
+ invokeScript tempDir scriptFile "build -t All" |> isProcessSucceeded "should build successfully"
}
- // Enable after https://github.com/fsharp/FAKE/pull/2403
- //yield test "can build with the local-style template" {
- // let tempDir = tempDir()
- // runTemplate tempDir Local Inline BuildTask
- // invokeScript tempDir scriptFile "build -t All" |> shouldSucceed "should build successfully"
- //}
+ yield test "can build with the local-style template" {
+ let tempDir = tempDir()
+ runTemplate tempDir Local Inline BuildTask
+ invokeScript tempDir scriptFile "build -t All" |> isProcessSucceeded "should build successfully"
+ }
/// ignored because the .net tool install to a subdirectory is broken: https://github.com/fsharp/FAKE/pull/1989#issuecomment-396057330
yield ptest "can install a tool-style template" {
@@ -198,7 +211,7 @@ let tests =
runTemplate tempDir Tool File Fake
Expect.isFalse (Directory.Exists (Path.Combine(tempDir, ".fake"))) "After creating the template the '.fake' directory should not exist!"
- invokeScript tempDir scriptFile "--help" |> shouldSucceed "should invoke help"
+ invokeScript tempDir scriptFile "--help" |> isProcessSucceeded "should invoke help"
}
]
]