using System; using System.Collections.Generic; using System.IO; using System.Linq; using SDKDocGenerator.Writers; using System.Diagnostics; namespace SDKDocGenerator { /// /// Documentation generator for the AWS SDK for .NET v3+ codebase. /// public class SdkDocGenerator { private long? _startTimeTicks; // These may be packaged in the assemblies folder for non-NuGet users, // but shouldn't appear in the SDK's API reference private static readonly IEnumerable _assembliesToSkip = new HashSet { "AWSSDK.Extensions.CrtIntegration.dll", "AWSSDK.Extensions.NETCore.Setup.dll" }; public GeneratorOptions Options { get; private set; } /// /// How long the documentation generation took. /// public TimeSpan Duration { get { if (!_startTimeTicks.HasValue) throw new InvalidOperationException("Execute(...) method has not been called, no run duration data available"); return new TimeSpan(DateTime.Now.Ticks - _startTimeTicks.Value); } } /// /// Manages the individual namespace tocs and consolidates them all into /// a single tree on the conclusion of processing. /// public TOCWriter TOCWriter { get; private set; } /// /// Runs the doc generator to produce or update a consistent documentation /// set for the SDK. /// /// /// 0 on successful completion public int Execute(GeneratorOptions options) { // this is just to record the run duration, so we can monitor and optimize // build-time perf _startTimeTicks = DateTime.Now.Ticks; Options = options; Trace.Listeners.Add(new ConditionalConsoleTraceListener(Options.Verbose)); if (Options.TestMode) SetOptionsForTestMode(); if (string.IsNullOrEmpty(Options.SDKAssembliesRoot)) { Info("ERROR: SDKAssembliesRoot option not set"); return -1; } if (Options.Verbose) { Info("Starting generation with options:"); Info("...TestMode: {0}", Options.TestMode); Info("...Clean: {0}", Options.Clean); Info("...WriteStaticContent: {0}", Options.WriteStaticContent); Info("...WaitOnExit: {0}", Options.WaitOnExit); Info(""); Info("...SDKAssembliesRoot: {0}", Options.SDKAssembliesRoot); Info("...OutputFolder: {0}", Options.OutputFolder); Info("...Platform: {0}", Options.Platform); Info("...Services: {0}", string.Join(",", Options.Services)); Info("...CodeSamplesRootFolder: {0}", Options.CodeSamplesRootFolder); Info(""); } if (options.Clean) FileUtilties.CleanFolder(options.OutputFolder, true); if (!Directory.Exists(options.OutputFolder)) Directory.CreateDirectory(options.OutputFolder); // use the sdk root and primary platform to determine the set of // service manifests to process var manifests = ConstructGenerationManifests(); TOCWriter = new TOCWriter(options); GenerationManifest coreManifest = null; DeferredTypesProvider deferredTypes = new DeferredTypesProvider(null); foreach (var m in manifests) { if (m.ServiceName.Equals("Core", StringComparison.InvariantCultureIgnoreCase)) { coreManifest = m; continue; } m.Generate(deferredTypes, TOCWriter); } // now all service assemblies are processed, handle core plus any types in those assemblies that // we elected to defer until we processed core. coreManifest.ManifestAssemblyContext.SdkAssembly.DeferredTypesProvider = deferredTypes; coreManifest.Generate(null, TOCWriter); Info("Generating table of contents entries..."); TOCWriter.Write(); CopyVersionInfoManifest(); if (options.WriteStaticContent) { Info("Generating/copying static content:"); Info("...creating landing page"); var lpWriter = new LandingPageWriter(options); lpWriter.Write(); Info("...copying static resources"); var sourceLocation = Directory.GetParent(typeof(SdkDocGenerator).Assembly.Location).FullName; FileUtilties.FolderCopy(Path.Combine(sourceLocation, "output-files"), options.OutputFolder, true); } // Write out all the redirect rules for doc cross-linking. using (Stream stream = File.Open(Path.Combine(options.OutputFolder, SDKDocRedirectWriter.RedirectFileName), FileMode.Create)) { SDKDocRedirectWriter.Write(stream); } return 0; } /// /// Sets the generation options so as to perform a limited generation pass on one /// platform and a handul of assemblies to allow verification of doc generator changes. /// private void SetOptionsForTestMode() { Info("Revising command line options to limited set for test mode:"); Options.WaitOnExit = true; Options.Verbose = true; if (string.IsNullOrEmpty(Options.SDKAssembliesRoot)) Options.SDKAssembliesRoot = @"..\..\..\..\deployment\assemblies"; Options.Services = new[] { "Core", "DynamoDBv2", "S3", "EC2" }; Info("...sdkassembliesroot set to '{0}'", Options.SDKAssembliesRoot); Info("...platform set to '{0}'", Options.Platform); Info("...services set to {0}", string.Join(",", Options.Services)); Info(""); } /// /// Copies the json file containing the version information for each SDK assembly /// (by service) into the output docs folder. /// private void CopyVersionInfoManifest() { Info("Copying version information manifest..."); if (File.Exists(Options.SDKVersionFilePath)) { var destPath = Path.Combine(Options.ComputedContentFolder, Path.GetFileName(Options.SDKVersionFilePath)); File.Copy(Options.SDKVersionFilePath, destPath, true); } else { throw new Exception($"Failed to find version file at {Options.SDKVersionFilePath}."); } } /// /// Enumerates the assembly, folder and platform settings in the options /// to construct a per-service manifest that we will then process. Our preferred /// documentation source is for the 'net45' platform but if a subfolder does /// not exist under the root for this platform, we'll generate using the /// assemblies in the first subfolder we find. /// /// /// Currently we only construct manifests for the assemblies we find in the /// preferred or first platform subfolder. Distinct assemblies that exist /// outside this folder do not get included. /// /// Collections of service manifests to process private IList ConstructGenerationManifests() { var platformSubfolders = Directory.GetDirectories(Options.SDKAssembliesRoot, "*", SearchOption.TopDirectoryOnly); var availablePlatforms = platformSubfolders.Select(Path.GetFileName).ToList(); if (!availablePlatforms.Any(ap => ap.Equals(Options.Platform, StringComparison.OrdinalIgnoreCase))) { Info("Warning: selected platform '{0}' is not available, switching to '{1}' for assembly discovery.", Options.Platform, availablePlatforms[0]); Options.Platform = availablePlatforms[0]; } var manifests = new List(); // discover the matching service/core assemblies in the selected platform folder and // construct a processing manifest for each var assemblySourcePath = Path.Combine(Options.SDKAssembliesRoot, Options.Platform); if (Options.Verbose) Info("Discovering assemblies in {0}", assemblySourcePath); foreach (var service in Options.Services) { var namePattern = $"{GenerationManifest.AWSAssemblyNamePrefix}.{service}.dll"; var assemblies = Directory.GetFiles(assemblySourcePath, namePattern); foreach (var assembly in assemblies) { if (_assembliesToSkip.Any(toSkip => assembly.Contains(toSkip))) { continue; } // keep items as the root for content, but a further subfolder of the root namespace // will be added for the artifacts var artifactManifest = new GenerationManifest(assembly, Options.ComputedContentFolder, availablePlatforms, Options, true); manifests.Add(artifactManifest); } } return manifests; } private void Info(string message) { Trace.WriteLine(message); Trace.Flush(); } private void Info(string format, params object[] args) { Trace.WriteLine(string.Format(format, args)); Trace.Flush(); } private void InfoVerbose(string message) { Trace.WriteLine(message, "Verbose"); Trace.Flush(); } private void InfoVerbose(string format, params object[] args) { Trace.WriteLine(String.Format(format, args), "Verbose"); Trace.Flush(); } } public class ConditionalConsoleTraceListener : TraceListener { private readonly bool _verboseOption; public ConditionalConsoleTraceListener(bool verbose) { this._verboseOption = verbose; } public override void Write(string message) { Console.Write(message); } public override void WriteLine(string message) { Console.WriteLine(message); } public override void WriteLine(string message, string category) { if (_verboseOption) { Console.WriteLine($"[{category}]: {message}"); } } } }