#light namespace Microsoft.FSharp.Build open System open System.Text open System.Diagnostics.CodeAnalysis open System.Reflection open Microsoft.Build.Framework open Microsoft.Build.Utilities open Internal.Utilities [] [] [] [] do() type FscCommandLineBuilder() = // In addition to generating a command-line that will be handed to cmd.exe, we also generate // an array of individual arguments. The former needs to be quoted (and cmd.exe will strip the // quotes while parsing), whereas the latter is not. See bug 4357 for background; this helper // class gets us out of the business of unparsing-then-reparsing arguments. let builder = new CommandLineBuilder() let mutable args = [] // in reverse order let mutable srcs = [] // in reverse order let mutable alreadyCalledWithFilenames = false /// Return a list of the arguments (with no quoting for the cmd.exe shell) member x.CapturedArguments() = assert(not alreadyCalledWithFilenames) List.rev args /// Return a list of the sources (with no quoting for the cmd.exe shell) member x.CapturedFilenames() = assert(alreadyCalledWithFilenames) List.rev srcs /// Return a full command line (with quoting for the cmd.exe shell) override x.ToString() = builder.ToString() member x.AppendFileNamesIfNotNull(filenames:ITaskItem array, sep:string) = builder.AppendFileNamesIfNotNull(filenames, sep) // do not update "args", not used for item in filenames do let tmp = new CommandLineBuilder() tmp.AppendSwitchUnquotedIfNotNull("", item.ItemSpec) // we don't want to quote the filename, this is a way to get that let s = tmp.ToString() if s <> String.Empty then srcs <- tmp.ToString() :: srcs alreadyCalledWithFilenames <- true member x.AppendSwitchIfNotNull(switch:string, values:string array, sep:string) = builder.AppendSwitchIfNotNull(switch, values, sep) let tmp = new CommandLineBuilder() tmp.AppendSwitchUnquotedIfNotNull(switch, values, sep) let s = tmp.ToString() if s <> String.Empty then args <- s :: args member x.AppendSwitchIfNotNull(switch:string, value:string) = builder.AppendSwitchIfNotNull(switch, value) let tmp = new CommandLineBuilder() tmp.AppendSwitchUnquotedIfNotNull(switch, value) let s = tmp.ToString() if s <> String.Empty then args <- s :: args member x.AppendSwitchUnquotedIfNotNull(switch:string, value:string) = assert(switch = "") // we only call this method for "OtherFlags" // Unfortunately we still need to mimic what cmd.exe does, but only for "OtherFlags". let ParseCommandLineArgs(commandLine:string) = // returns list in reverse order let mutable args = [] let mutable i = 0 // index into commandLine let len = commandLine.Length while i < len do // skip whitespace while i < len && System.Char.IsWhiteSpace(commandLine, i) do i <- i + 1 if i < len then // parse an argument let sb = new StringBuilder() let mutable finished = false let mutable insideQuote = false while i < len && not finished do match commandLine.[i] with | '"' -> insideQuote <- not insideQuote; i <- i + 1 | c when not insideQuote && System.Char.IsWhiteSpace(c) -> finished <- true | c -> sb.Append(c) |> ignore; i <- i + 1 args <- sb.ToString() :: args args builder.AppendSwitchUnquotedIfNotNull(switch, value) let tmp = new CommandLineBuilder() tmp.AppendSwitchUnquotedIfNotNull(switch, value) let s = tmp.ToString() if s <> String.Empty then args <- ParseCommandLineArgs(s) @ args member x.AppendSwitch(switch:string) = builder.AppendSwitch(switch) args <- switch :: args // for unit testing (attaching a Logger listener) [] type FscCustomBuildEventArgs(commandLine:string) = inherit CustomBuildEventArgs() member this.CommandLine = commandLine //There are a lot of flags on fsc.exe. //For now, not all of them are represented in the "Fsc class" object model. //The goal is to have the most common/important flags available via the Fsc class, and the //rest can be "backdoored" through the .OtherFlags property. type [] Fsc() = inherit ToolTask() let mutable baseAddress : string = null let mutable codePage : string = null let mutable debugSymbols = false let mutable debugType : string = null let mutable defineConstants : ITaskItem[] = [||] let mutable disabledWarnings : string[] = [||] let mutable documentationFile : string = null let mutable generateInterfaceFile : string = null let mutable keyFile : string = null let mutable noFramework = false let mutable optimize : bool = true let mutable tailcalls : bool = true let mutable otherFlags : string = null let mutable outputAssembly : string = null let mutable pdbFile : string = null let mutable platform : string = null let mutable references : ITaskItem[] = [||] let mutable referencePath : string = null let mutable resources : ITaskItem[] = [||] let mutable sources : ITaskItem[] = [||] let mutable targetType : string = null #if FX_ATLEAST_35 #else let mutable toolExe : string = "fsc.exe" #endif let mutable nowarn : string = null let mutable warningLevel : string = null let mutable treatWarningsAsErrors : bool = false let mutable warningsAsErrors : string = null let mutable toolPath : string = match FSharpEnvironment.FSharpRunningBinFolder with | Some(s) -> s | None -> "" let mutable versionFile : string = null let mutable win32res : string = null let mutable win32manifest : string = null let mutable vserrors : bool = false let mutable utf8output : bool = false let mutable capturedArguments : string list = [] // list of individual args, to pass to HostObject Compile() let mutable capturedFilenames : string list = [] // list of individual source filenames, to pass to HostObject Compile() // --baseaddress member fsc.BaseAddress with get() = baseAddress and set(s) = baseAddress <- s // --codepage : Specify the codepage to use when opening source files member fsc.CodePage with get() = codePage and set(s) = codePage <- s // -g: Produce debug file. Disables optimizations if a -O flag is not given. member fsc.DebugSymbols with get() = debugSymbols and set(b) = debugSymbols <- b // --debug : Emit debugging information member fsc.DebugType with get() = debugType and set(s) = debugType <- s // --nowarn : Do not report the given specific warning. member fsc.DisabledWarnings with get() = disabledWarnings and set(a) = disabledWarnings <- a // --define : Define the given conditional compilation symbol. member fsc.DefineConstants with get() = defineConstants and set(a) = defineConstants <- a // --doc : Write the xmldoc of the assembly to the given file. member fsc.DocumentationFile with get() = documentationFile and set(s) = documentationFile <- s // --generate-interface-file : // Print the inferred interface of the // assembly to a file. member fsc.GenerateInterfaceFile with get() = generateInterfaceFile and set(s) = generateInterfaceFile <- s // --keyfile : // Sign the assembly the given keypair file, as produced // by the .NET Framework SDK 'sn.exe' tool. This produces // an assembly with a strong name. This is only relevant if producing // an assembly to be shared amongst programs from different // directories, e.g. to be installed in the Global Assembly Cache. member fsc.KeyFile with get() = keyFile and set(s) = keyFile <- s // --noframework member fsc.NoFramework with get() = noFramework and set(b) = noFramework <- b // --optimize member fsc.Optimize with get() = optimize and set(p) = optimize <- p // --tailcalls member fsc.Tailcalls with get() = tailcalls and set(p) = tailcalls <- p // REVIEW: decide whether to keep this, for now is handy way to deal with as-yet-unimplemented features member fsc.OtherFlags with get() = otherFlags and set(s) = otherFlags <- s // -o : Name the output file. member fsc.OutputAssembly with get() = outputAssembly and set(s) = outputAssembly <- s // --pdb : // Name the debug output file. member fsc.PdbFile with get() = pdbFile and set(s) = pdbFile <- s // --platform : Limit which platforms this code can run on: // x86 // x64 // Itanium // anycpu member fsc.Platform with get() = platform and set(s) = platform <- s // -r : Reference an F# or .NET assembly. member fsc.References with get() = references and set(a) = references <- a // --lib member fsc.ReferencePath with get() = referencePath and set(s) = referencePath <- s // --resource : Embed the specified managed resources (.resource). // Produce .resource files from .resx files using resgen.exe or resxc.exe. member fsc.Resources with get() = resources and set(a) = resources <- a // source files member fsc.Sources with get() = sources and set(a) = sources <- a // --target exe: Produce an executable with a console // --target winexe: Produce an executable which does not have a // stdin/stdout/stderr // --target library: Produce a DLL // --target module: Produce a module that can be added to another assembly member fsc.TargetType with get() = targetType and set(s) = targetType <- s // --version-file : member fsc.VersionFile with get() = versionFile and set(s) = versionFile <- s #if FX_ATLEAST_35 #else // Allow overriding to the executable name "fsc.exe" member fsc.ToolExe with get() = toolExe and set(s) = toolExe<- s #endif // For targeting other folders for "fsc.exe" (or ToolExe if different) member fsc.ToolPath with get() = toolPath and set(s) = toolPath <- s // For specifying a win32 native resource file (.res) member fsc.Win32ResourceFile with get() = win32res and set(s) = win32res <- s // For specifying a win32 manifest file member fsc.Win32ManifestFile with get() = win32manifest and set(m) = win32manifest <- m // For specifying warnings to disable member fsc.NoWarn with get() = nowarn and set(s) = nowarn <- s // For specifying the warning level (0-4) member fsc.WarningLevel with get() = warningLevel and set(s) = warningLevel <- s member fsc.TreatWarningsAsErrors with get() = treatWarningsAsErrors and set(p) = treatWarningsAsErrors <- p member fsc.WarningsAsErrors with get() = warningsAsErrors and set(s) = warningsAsErrors <- s member fsc.VisualStudioStyleErrors with get() = vserrors and set(p) = vserrors <- p member fsc.Utf8Output with get() = utf8output and set(p) = utf8output <- p // ToolTask methods override fsc.ToolName = "fsc.exe" override fsc.StandardErrorEncoding = if utf8output then System.Text.Encoding.UTF8 else base.StandardErrorEncoding override fsc.StandardOutputEncoding = if utf8output then System.Text.Encoding.UTF8 else base.StandardOutputEncoding override fsc.GenerateFullPathToTool() = if toolPath = "" then // REVIEW factor out string resource below raise (new System.InvalidOperationException("ToolPath is unknown; specify the path to fsc.exe as the ToolPath property.")) System.IO.Path.Combine(toolPath, fsc.ToolExe) member internal fsc.BaseExecuteTool(pathToTool, responseFileCommands, commandLineCommands) = // F# does not allow protected members to be captured by lambdas, this is the standard workaround base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands) /// Intercept the call to ExecuteTool to handle the host compile case. override fsc.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands) = let ho = box fsc.HostObject match ho with | null -> base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands) | _ -> let sources = sources|>Array.map(fun i->i.ItemSpec) let baseCall = fun (dummy : int) -> fsc.BaseExecuteTool(pathToTool, responseFileCommands, commandLineCommands) // We are using a Converter rather than a "unit->int" because it is too hard to // figure out how to pass an F# function object via reflection. let baseCallDelegate = new System.Converter(baseCall) try let ret = (ho.GetType()).InvokeMember("Compile", BindingFlags.Public ||| BindingFlags.InvokeMethod ||| BindingFlags.Instance, null, ho, [| box baseCallDelegate; box (capturedArguments |> List.to_array); box (capturedFilenames |> List.to_array) |], System.Globalization.CultureInfo.InvariantCulture) unbox ret with e -> System.Diagnostics.Debug.Assert(false, "HostObject received by Fsc task did not have a Compile method or the compile method threw an exception. "^(e.ToString())) rethrow() override fsc.GenerateCommandLineCommands() = let builder = new FscCommandLineBuilder() // OutputAssembly builder.AppendSwitchIfNotNull("-o:", outputAssembly) // CodePage builder.AppendSwitchIfNotNull("--codepage:", codePage) // Debug if debugSymbols then builder.AppendSwitch("-g") // DebugType builder.AppendSwitchIfNotNull("--debug:", if debugType = null then null else match debugType.ToUpperInvariant() with | "NONE" -> null | "PDBONLY" -> "pdbonly" | "FULL" -> "full" | _ -> null) // NoFramework if noFramework then builder.AppendSwitch("--noframework") // BaseAddress builder.AppendSwitchIfNotNull("--baseaddress:", baseAddress) // DefineConstants for item in defineConstants do builder.AppendSwitchIfNotNull("--define:", item.ItemSpec) // DisabledWarnings for disabledWarning in disabledWarnings do builder.AppendSwitchIfNotNull("--nowarn:", disabledWarning) // DocumentationFile builder.AppendSwitchIfNotNull("--doc:", documentationFile) // GenerateInterfaceFile builder.AppendSwitchIfNotNull("--sig:", generateInterfaceFile) // KeyFile builder.AppendSwitchIfNotNull("--keyfile:", keyFile) // Optimize if optimize then builder.AppendSwitch("--optimize+") else builder.AppendSwitch("--optimize-") if not tailcalls then builder.AppendSwitch("--tailcalls-") // PdbFile builder.AppendSwitchIfNotNull("--pdb:", pdbFile) // Platform builder.AppendSwitchIfNotNull("--platform:", if platform = null then null else match platform.ToUpperInvariant() with | "ANYCPU" -> "anycpu" | "X86" -> "x86" | "X64" -> "x64" | "ITANIUM" -> "Itanium" | _ -> null) // Resources for item in resources do builder.AppendSwitchIfNotNull("--resource:", item.ItemSpec) // VersionFile builder.AppendSwitchIfNotNull("--versionfile:", versionFile) // References for item in references do builder.AppendSwitchIfNotNull("-r:", item.ItemSpec) // ReferencePath let referencePathArray = // create a array of strings match referencePath with | null -> null | _ -> referencePath.Split([|' '; ';'; ','|], StringSplitOptions.RemoveEmptyEntries) builder.AppendSwitchIfNotNull("--lib:", referencePathArray, ",") // TargetType builder.AppendSwitchIfNotNull("--target:", if targetType = null then null else match targetType.ToUpperInvariant() with | "LIBRARY" -> "library" | "EXE" -> "exe" | "WINEXE" -> "winexe" | "MODULE" -> "module" | _ -> null) // NoWarn let noWarnArray = // create a array of strings match nowarn with | null -> null | _ -> nowarn.Split([|' '; ';'; ','|], StringSplitOptions.RemoveEmptyEntries) builder.AppendSwitchIfNotNull("--nowarn:", noWarnArray, ",") // WarningLevel builder.AppendSwitchIfNotNull("--warn:", warningLevel) // TreatWarningsAsErrors if treatWarningsAsErrors then builder.AppendSwitch("--warnaserror") // WarningsAsErrors // Change warning 76, HashReferenceCopyAfterCompileNotAllowedInNonScript, into an error let warningsAsErrorsArray = match warningsAsErrors with | null -> [|"76"|] | _ -> (warningsAsErrors ^ " 76 ").Split([|' '; ';'; ','|], StringSplitOptions.RemoveEmptyEntries) builder.AppendSwitchIfNotNull("--warnaserror:", warningsAsErrorsArray, ",") // Win32ResourceFile builder.AppendSwitchIfNotNull("--win32res:", win32res) // Win32ManifestFile builder.AppendSwitchIfNotNull("--win32manifest:", win32manifest) // VisualStudioStyleErrors if vserrors then builder.AppendSwitch("--vserrors") if utf8output then builder.AppendSwitch("--utf8output") // When building using the fsc task, always emit the "fullpaths" flag to make the output easier // for the user to parse builder.AppendSwitch("--fullpaths") // When building using the fsc task, also emit "flaterrors" to ensure that multi-line error messages // aren't trimmed builder.AppendSwitch("--flaterrors") // OtherFlags - must be second-to-last builder.AppendSwitchUnquotedIfNotNull("", otherFlags) capturedArguments <- builder.CapturedArguments() // Sources - these have to go last builder.AppendFileNamesIfNotNull(sources, " ") capturedFilenames <- builder.CapturedFilenames() let s = builder.ToString() if fsc.BuildEngine <> null then // when doing simple unit tests using API, no BuildEnginer/Logger is attached fsc.BuildEngine.LogCustomEvent( new FscCustomBuildEventArgs(s) ) s // expose this to internal components (for nunit testing) member internal fsc.InternalGenerateCommandLineCommands() = fsc.GenerateCommandLineCommands() member internal fsc.InternalExecuteTool(pathToTool, responseFileCommands, commandLineCommands) = fsc.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands) module Attributes = //[] do()