diff --git a/BSABR/BSABR.sln b/BSABR/BSABR.sln new file mode 100644 index 0000000..7745403 --- /dev/null +++ b/BSABR/BSABR.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31624.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BSABR", "BSABR\BSABR.csproj", "{7B474A17-7931-4385-A6C3-0045B17FD9EC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7B474A17-7931-4385-A6C3-0045B17FD9EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B474A17-7931-4385-A6C3-0045B17FD9EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B474A17-7931-4385-A6C3-0045B17FD9EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B474A17-7931-4385-A6C3-0045B17FD9EC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E5B22D7B-0383-41CE-B986-DDC48207B64B} + EndGlobalSection +EndGlobal diff --git a/BSABR/BSABR/App.config b/BSABR/BSABR/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/BSABR/BSABR/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BSABR/BSABR/ArgParser.cs b/BSABR/BSABR/ArgParser.cs new file mode 100644 index 0000000..52f9340 --- /dev/null +++ b/BSABR/BSABR/ArgParser.cs @@ -0,0 +1,57 @@ +// Copyright © 2021 Matt Sullivan + +using System.Linq; + +namespace Bsabr +{ + /// + /// Extracts string arguments for B-SABR into a more easy to use object. + /// + class ArgParser + { + private readonly string[] _args; + + /// + /// Initializes a new instance of the class. + /// + /// The arguments to extract. + private ArgParser(string[] args) + { + _args = args; + } + + /// + /// Gets a new instance of the class. + /// + /// The arguments that the ArgParser should process. + /// An for the specified . + public static ArgParser InitializeNew(string[] args) + { + return new ArgParser(args); + } + + /// + /// Gets the options that this 's arguments specified. + /// + /// The options that were represented by the arguments. + internal Options GetOptions() + { + // help will have been requested if the first of the arguments match a normal help string + var helpRequested = _args.FirstOrDefault() == "/?" || _args.FirstOrDefault() == "/help"; + + // studio file name will be the first argument (as long as that wasn't the help arg) + var studioFileName = helpRequested ? null : _args.FirstOrDefault(); + + // whether or not the caller is requesting build output to be saved to a file + var outFileSpecified = _args.Any(a => a == "/out"); + + // output filename will be the first argument after the "/out" filename + var outFileName = outFileSpecified ? _args.SkipWhile(a => a != "/out").Skip(1).FirstOrDefault() : null; + + // studio arguments will be all of the arguments after the first argument + var studioArgs = helpRequested ? null : string.Join(" ", _args.Skip(1)); + + return new Options(studioFileName, outFileName, outFileSpecified, studioArgs, helpRequested); + } + } +} diff --git a/BSABR/BSABR/ArgValidator.cs b/BSABR/BSABR/ArgValidator.cs new file mode 100644 index 0000000..cf5e83c --- /dev/null +++ b/BSABR/BSABR/ArgValidator.cs @@ -0,0 +1,155 @@ +// Copyright © 2021 Matt Sullivan + +using System; +using System.IO; +using System.Text; + +namespace Bsabr +{ + /// + /// Validates options for the process instance. + /// + class ArgValidator + { + private readonly Options _options; + private readonly string _errorMessage; + + /// + /// Initializes a new instance of the class. + /// + /// The to validate. + public ArgValidator(Options options) + { + _options = options; + + if (!_options.HelpRequested) + { + _errorMessage = GetErrorMessage(); + } + } + + /// + /// Summary of what is wrong with the options. + /// + public string ErrorMessage => _errorMessage; + + /// + /// Indicates whether or not the options for this are valid. + /// + public bool OptionsAreValid => _errorMessage == null; + + /// + /// Determines whether an is suitable for the program to execute. + /// + /// The configuration to validate. + /// An error message, if an error occurred. null otherwise. + private string GetErrorMessage() + { + var errorBuilder = new StringBuilder(); + + var studioExecutableErrorMessage = GetStudioExecutableErrorString(); + + if (studioExecutableErrorMessage != null) + { + errorBuilder.AppendLine($"\t- {studioExecutableErrorMessage}"); + } + + var outFileNameErrorMessage = GetOutFileNameErrorString(); + + if (outFileNameErrorMessage != null) + { + errorBuilder.AppendLine($"\t- {outFileNameErrorMessage}"); + } + + if (errorBuilder.Length > 0) + { + return $"The following error(s) occurred: \n{errorBuilder}"; + } + + return null; + } + + /// + /// Gets an error string based on the specified output filename. + /// + /// An error message if the argument is invalid, null otherwise. + private string GetOutFileNameErrorString() + { + if (_options.OutFileName == null) + { + if (_options.OutFileSpecified) + { + return "The \"/out\" argument should be followed by a valid file path."; + } + + return null; + } + + try + { + var attributes = File.GetAttributes(_options.OutFileName); + } + catch (ArgumentException) + { + return "Invalid output file path. Path is empty, contains only white spaces, or contains invalid characters."; + } + catch (PathTooLongException) + { + return "Output file path is too long. File paths should be less than 260 characters."; + } + catch (NotSupportedException) + { + return "Output file path is in an invalid format."; + } + catch (FileNotFoundException) + { + // Not a problem in this scenario as we will be creating the file + } + catch (DirectoryNotFoundException) + { + return "Output file path represents a directory and is invalid, such as being on an unmapped drive, or the directory cannot be found."; + } + catch (IOException) + { + return "Output file path is to a file that is in use by another process."; + } + catch (UnauthorizedAccessException) + { + return "You do not have permission to access the file specified as the output file path argument."; + } + + return null; + } + + /// + /// Gets an error string based on the specified path to AtmelStudio.exe. + /// + /// An error message if the argument is invalid, null otherwise. + private string GetStudioExecutableErrorString() + { + bool fileExists = File.Exists(_options.StudioExecutable); + + if (!fileExists) + { + // see if it exists on the path somewhere + foreach (var evt in Enum.GetValues(typeof(EnvironmentVariableTarget))) + { + var paths = Environment.GetEnvironmentVariable("PATH", (EnvironmentVariableTarget)evt).Split(';'); + + foreach (var path in paths) + { + if (File.Exists(_options.StudioExecutable)) + { + fileExists = true; + break; + } + } + } + + return "The specified Atmel Studio/Microchip Studio executable does not exist. Make sure you have entered the path correctly and that you have permission to access the executable."; + } + + return null; + } + } +} diff --git a/BSABR/BSABR/BSABR.csproj b/BSABR/BSABR/BSABR.csproj new file mode 100644 index 0000000..0b1b006 --- /dev/null +++ b/BSABR/BSABR/BSABR.csproj @@ -0,0 +1,86 @@ + + + + + Debug + AnyCPU + {7B474A17-7931-4385-A6C3-0045B17FD9EC} + Exe + Bsabr + bsabr + v4.7.2 + 512 + true + true + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + + + + + False + Microsoft .NET Framework 4.7.2 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + + + \ No newline at end of file diff --git a/BSABR/BSABR/Options.cs b/BSABR/BSABR/Options.cs new file mode 100644 index 0000000..a3f66c4 --- /dev/null +++ b/BSABR/BSABR/Options.cs @@ -0,0 +1,51 @@ +// Copyright © 2021 Matt Sullivan + +namespace Bsabr +{ + /// + /// Represents the command line options selected when the process was started. + /// + class Options + { + /// + /// Initializes a new instance of the class. + /// + /// Path to the Atmel Studio/Microchip Studio executable. + /// The name of the output file requested in the process arguments. null if no /out argument was supplied. + /// The arguments that should be passed to Atmel Studio/Microchip Studio. + /// Indicates whether or not a help argument was passed in at process start. + public Options(string studioExecutable, string outFileName, bool outFileSpecified, string studioArgs, bool helpRequested) + { + StudioExecutable = studioExecutable; + OutFileName = outFileName; + OutFileSpecified = outFileSpecified; + StudioArgs = studioArgs; + HelpRequested = helpRequested; + } + + /// + /// The name of the output file requested in the process arguments. Is null when no /out argument was supplied. + /// + public string OutFileName { get; } + + /// + /// Whether or not the user specified the "/out" argument for AtmelStudio.exe. + /// + public bool OutFileSpecified { get; } + + /// + /// Path to the Atmel Studio/Microchip Studio executable. + /// + public string StudioExecutable { get; } + + /// + /// The arguments that should be passed to Atmel Studio/Microchip Studio. + /// + public string StudioArgs { get; } + + /// + /// Indicates whether or not a help argument was passed in at process start. + /// + public bool HelpRequested { get; } + } +} diff --git a/BSABR/BSABR/Program.cs b/BSABR/BSABR/Program.cs new file mode 100644 index 0000000..e5e9660 --- /dev/null +++ b/BSABR/BSABR/Program.cs @@ -0,0 +1,169 @@ +// Copyright © 2021 Matt Sullivan + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace Bsabr +{ + class Program + { + // regex for the file finished string is a match when there are any number of characters, + // and the final line matches with equals signs, Build succeeded/up-to-date, failed, an + // skipped count all have one or more decimal, and there are closing equal signs. + // This is the end of the string. + private static readonly Regex _buildFinishedRegex = new Regex(@".*========== Build: \d+ succeeded or up-to-date, \d+ failed, \d+ skipped =========="); + + static void Main(string[] args) + { + var (shouldContinue, options) = ParseArguments(args); + + if (!shouldContinue) + { + return; + } + + // we need to specify an out file if the user didn't specify one + var outFileName = options.OutFileName ?? Path.GetTempFileName(); + + // start with an empty file + if (File.Exists(outFileName)) + { + File.Delete(outFileName); + } + + // we need an output file - that's how this whole thing works! if the user didn't specify one, we will + // add an argument for it to the arguments that we pass to AtmelStudio.exe + var studioArgs = options.OutFileSpecified ? options.StudioArgs : $"{options.StudioArgs} /out {outFileName}"; + + RunBuild(options.StudioExecutable, studioArgs, outFileName); + + // clean up if the user didn't specify that they wanted an output file of the build + if (!options.OutFileSpecified) + { + DeleteOutFile(outFileName); + } + } + + /// + /// Parses the command line arguments, validating them, indicates the options that the caller + /// selected and whether or not to continue execution of the program. + /// + /// Command line arguments passed to the program. + /// A indicating whether execution of the program should continue, + /// and if so, which execution options the specify. + private static (bool shouldContinue, Options options) ParseArguments(string[] args) + { + var argParser = ArgParser.InitializeNew(args); + var options = argParser.GetOptions(); + var validator = new ArgValidator(options); + + var shouldShowHelp = options.HelpRequested; + + if (!validator.OptionsAreValid) + { + Console.WriteLine(validator.ErrorMessage); + shouldShowHelp = true; + } + + if (shouldShowHelp) + { + Console.WriteLine("Usage: Bsabr.exe studio_path studio_arguments\n" + + "\tstudio_path: the path to AtmelStudio.exe" + + "\tstudio_arguments: the build arguments to pass to AtmelStudio.exe" + + "If you are unsure of which arguments you should use for Atmel Studio" + + "/Microchip Studio, launch it from the command line with the \"/?\"" + + "argument to see its usage"); + + return (false, options); + } + + return (true, options); + } + + /// + /// Runs the build, forwarding all build output to the console and effectively blocking until the build is finished. + /// + /// The path to AtmelStudio.exe. + /// Arguments to be passed to AtmelStudio.exe. + /// The path to the file where build output will be saved. + private static void RunBuild(string studioExecutablePath, string studioArgs, string outFilePath) + { + // this is where the magic happens. we simply monitor the build output + // as it is written to a file and forward that to the console. + // we know when the build is done because studio always prints the + // same format of build summary at the end + + using (var logFileStream = new FileStream(outFilePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)) + using (var logFileStreamReader = new StreamReader(logFileStream)) + { + var studioProcess = Process.Start(new ProcessStartInfo + { + FileName = studioExecutablePath, + Arguments = studioArgs, + }); + + var buildOutputContent = new StringBuilder(); + + while (!_buildFinishedRegex.IsMatch(buildOutputContent.ToString())) + { + if (!logFileStreamReader.EndOfStream) + { + var currentString = logFileStreamReader.ReadToEnd(); + + if (!string.IsNullOrEmpty(currentString)) + { + buildOutputContent.Append(currentString); + Console.Write(currentString); + } + } + + if (studioProcess.HasExited && studioProcess.ExitCode != 0) + { + // during a normal build, studio will have an exit code of 0. + // but if an error occurred, let's stop waiting for a build to finish + break; + } + } + } + } + + /// + /// Deletes the out file. + /// + /// + private static void DeleteOutFile(string outFilePath) + { + var fileDeleted = false; + var errorDeletingFile = false; + var tryDeleteStartTime = DateTime.UtcNow; + var timeoutDuration = TimeSpan.FromSeconds(20); + + while (!fileDeleted && timeoutDuration < DateTime.UtcNow - tryDeleteStartTime) + { + // this is in a loop since sometimes the file is still locked by the compiler + try + { + File.Delete(outFilePath); + + fileDeleted = true; + } + catch (IOException) + { + // file is still locked by the compiler - loop back and try again + } + catch (Exception) + { + errorDeletingFile = true; + } + } + + if (errorDeletingFile) + { + Console.WriteLine($"A temporary file was created but could not be deleted. Location:\n\t{outFilePath}."); + } + } + } +} diff --git a/BSABR/BSABR/Properties/AssemblyInfo.cs b/BSABR/BSABR/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c360790 --- /dev/null +++ b/BSABR/BSABR/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("B-SABR")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("B-SABR")] +[assembly: AssemblyCopyright("Copyright © 2021 Matt Sullivan")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("7b474a17-7931-4385-a6c3-0045b17fd9ec")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.0.1.0")] +[assembly: AssemblyFileVersion("0.0.1.0")] diff --git a/README.md b/README.md index 0156e79..48d47cf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # b-sabr -B-SABR - Blocking SAM & AVR Build Runner +B-SABR: Blocking SAM & AVR Build Runner. + +B-SABR is a utility for building Atmel Studio/Microchip Studio solutions at the command line. This utility exists because Atmel Studio/Microchip Studio: + +* does not support blocking builds (i.e. the invoking executable staying alive until the build is finished) +* does not support directing build output to the console + +Both of these are desirable traits in a build executable for various reasons and use cases. + +## How to use B-SABR +B-SABR is a minimalist wrapper for ```AtmelStudio.exe```. Therefore, it runs without interaction, and supports a very simple command line argument format. + +``` +Usage: bsabr.exe studio_path studio_arguments + studio_path: the path to AtmelStudio.exe + studio_arguments: the build arguments to pass to AtmelStudio.exe +``` + +If you are unsure of which arguments you should use for AtmelStudio.exe, launch it from the command line with the "/?" argument to see its usage. + +### An example usage of B-SABR +```bsabr.exe "C:\Program Files (x86)\Atmel\Studio\7.0\AtmelStudio.exe" "C:\code\asf_test\asf_test.atsln" /build DEBUG``` + +## Help, I'm not seeing any build output! +Based on the information found in [this AvrFreaks thread](https://www.avrfreaks.net/forum/see-complete-log-command-line-build), make sure your build verbosity is appropriately set. If you haven't run the AtmelStudio.exe GUI yet, do so. If you cannot run the AtmelStudio.exe GUI, then set the appropriate registry values as specified in [this post](https://www.avrfreaks.net/comment/2874201#comment-2874201). + +## Intellectual Property Notice +AVR, SAM, Atmel Studio, and Microchip Studio are all trademarks of Microchip Technology, Inc.