/// <summary> /// /// </summary> /// <param name="packageName">eg: com.unity.package-manager-ui</param> /// <param name="shortVersionId">eg: [email protected] (note: not @1.2.0)</param> ///<param name="siteFolder">Output folder where the doc site should be created.</param> public virtual void Build(string packageName, string shortVersionId, string siteFolder = null) { if (string.IsNullOrEmpty(siteFolder)) { siteFolder = GetPackageSiteFolder(shortVersionId); } string packageFolder = Path.GetFullPath(string.Format("packages/{0}", packageName)); string buildFolder = GetPackageBuildFolder(shortVersionId); string docfxJson = Path.Combine(buildFolder, "docfx.json"); string logoSource = Path.Combine(buildFolder, "logo.svg"); string logoTarget = Path.Combine(siteFolder, "logo.svg"); string manualSource = Path.Combine(buildFolder, "manual"); if (!Directory.Exists(MonoRootPath)) { ZipUtils.Unzip(MonoZip, MonoRootPath); } // // Setup Mono for Mac/Linux if (Application.platform != RuntimePlatform.WindowsEditor) { var monoPath = Path.Combine(DocumentationRoot, "mono"); if (!Directory.Exists(monoPath)) { FileUtil.CopyFileOrDirectory(MonoRootPath, monoPath); SetFolderPermission(monoPath, "777"); } MonoPath = Path.Combine(monoPath, "bin/mono-sgen64"); } // // Set target folders to initial condition Directory.CreateDirectory(buildFolder); // Clear build folder FileUtil.DeleteFileOrDirectory(buildFolder); // Clear site folder prior to building FileUtil.DeleteFileOrDirectory(siteFolder); // // Prepare build folder string packageCloneFolder = Path.Combine(buildFolder, "package"); FileUtil.CopyFileOrDirectory(DocFxTemplateRoot, buildFolder); FileUtil.CopyFileOrDirectory(packageFolder, packageCloneFolder); // Remove ignored folders in the generated doc foreach (string dirPath in Directory.GetDirectories(packageCloneFolder, ".*", SearchOption.AllDirectories)) { FileUtil.DeleteFileOrDirectory(dirPath); } foreach (string dirPath in Directory.GetDirectories(packageCloneFolder, "*~", SearchOption.AllDirectories)) { FileUtil.DeleteFileOrDirectory(dirPath); } // When used from the cache (which is read-only), it is important to set the folder properties to write. SetFolderPermission(buildFolder); // Prepare feature doc for docfx string packageDocs = Path.Combine(packageFolder, "Documentation"); string packageDocsTilde = Path.Combine(packageFolder, "Documentation~"); string packageDocsDot = Path.Combine(packageFolder, ".Documentation"); if (Directory.Exists(packageDocsTilde)) { packageDocs = packageDocsTilde; } else if (Directory.Exists(packageDocsDot)) { packageDocs = packageDocsDot; } var sourceManualFiles = Directory.GetFiles(packageDocs, "*.md", SearchOption.AllDirectories); if (Directory.Exists(packageDocs) && sourceManualFiles.Any()) { DirectoryCopy(packageDocs, manualSource); } else { Debug.LogWarning("Package Documentation Build: Feature Documentation does not exist. An empty placeholder will be used instead"); string manualFile = Path.Combine(manualSource, "index.md"); File.WriteAllText(manualFile, "Information\n-----------\n<span style=\"color:red\">There is currently no documentation for this package.</span>"); } var filterFilesource = Path.Combine(manualSource, "filter.yml"); if (File.Exists(filterFilesource)) { SetFolderPermission(manualSource, "777"); var filterTarget = Path.Combine(buildFolder, "filter.yml"); File.Delete(filterTarget); File.Copy(filterFilesource, filterTarget); File.Delete(filterFilesource); } var projectMetadataFileSource = Path.Combine(manualSource, "projectMetadata.json"); if (File.Exists(projectMetadataFileSource)) { SetFolderPermission(manualSource, "777"); var projectMetadataFileTarget = Path.Combine(buildFolder, "projectMetadata.json"); File.Delete(projectMetadataFileTarget); File.Copy(projectMetadataFileSource, projectMetadataFileTarget); File.Delete(projectMetadataFileSource); } // Setup the default title - replace all appearances of `DEFAULT_APP_TITLE` with the real default `PACKAGE DISPLAY NAME | VERSION` SetupDefaultTitle(packageFolder, buildFolder); // Generate table of content for manual var docfxToc = Path.Combine(manualSource, "toc.md"); if (File.Exists(docfxToc)) { Debug.LogError("Your table of content file is named 'toc.md' and should be named 'TableOfContents.md'. Please rename it."); } var docWorksToC = Path.Combine(manualSource, "TableOfContents.md"); if (File.Exists(docWorksToC)) { // DocWorks ToC support // Index.md is necessary for DocFx for now since it's embedded in the template. if (!File.Exists(Path.Combine(manualSource, "index.md"))) { Debug.LogError("Package Documentation generation error. Need an index.md when using a toc.md. Resulting site will not work."); return; } docfxJson = Path.Combine(buildFolder, "docfx_toc_enabled.json"); // Force ToC to be enabled on manual page // Convert DocWorks style ToC to DocFx ToC format ConvertDocWorksToC(docWorksToC, Path.Combine(buildFolder, "manual/toc.md")); } else { // // Simple manual support var manualFiles = Directory.GetFiles(manualSource, "*.md", SearchOption.AllDirectories); var manualIndexCandidateFile = manualFiles.First(); var manualIndexFile = Path.Combine(manualSource, "index.md"); if (!File.Exists(manualIndexFile)) { FileUtil.MoveFileOrDirectory(manualIndexCandidateFile, manualIndexFile); } // Landing page for feature doc is alway index.md since that's required to make sure // that the docfx home page always redirects to this specific url. string ymlToc = "- name: Introduction\n href: index.md\n"; string manualToc = Path.Combine(manualSource, "toc.yml"); File.WriteAllText(manualToc, ymlToc); } // Add License section string ymlLicenseToc = string.Format("- name: {0}\n href: LICENSE.md\n", "License"); // Will create an index.html that can always be linked to that will redirect either to third party license (if any) or the companion license string licenseIndexFile = Path.Combine(buildFolder, "license/index.md"); string licenseIndexName = "LICENSE"; var licenseFile = Path.Combine(packageFolder, "LICENSE.md"); if (!File.Exists(licenseFile)) { licenseFile = Path.Combine(packageFolder, "LICENSE"); } if (File.Exists(licenseFile)) { File.Copy(licenseFile, Path.Combine(buildFolder, "license/LICENSE.md"), true); } else { throw new Exception("No valid LICENSE.md file found at the root of the package"); } var licenseThirdPartyFile = Path.Combine(packageFolder, "Third Party Notices.md"); var licenseThirdPartyFileTarget = Path.Combine(buildFolder, "license/Third Party Notices.md"); if (File.Exists(licenseThirdPartyFile)) { File.Copy(licenseThirdPartyFile, licenseThirdPartyFileTarget, true); licenseIndexName = "Third%20Party%20Notices"; ymlLicenseToc = "- name: Third Party Notices\n href: Third Party Notices.md\n" + ymlLicenseToc; } File.WriteAllText(licenseIndexFile, string.Format("<script>window.location.replace('{0}.html')</script>", licenseIndexName)); string licenseToc = Path.Combine(buildFolder, "license/toc.yml"); File.WriteAllText(licenseToc, ymlLicenseToc); var versionToken = shortVersionId.Split('@')[1]; Version version; Version.TryParse(versionToken, out version); if (packageName == "com.unity.tiny" && version < new Version(0, 15)) { File.Copy(Path.Combine(buildFolder, "toc_tiny.yml"), Path.Combine(buildFolder, "toc.yml"), true); docfxJson = Path.Combine(buildFolder, "docfx_toc_tiny.json"); CreateTinyRuntimeDoc(buildFolder); } // Use the changelog, otherwise keep the empty default changelog. var changelogFile = Path.Combine(packageFolder, "CHANGELOG.md"); if (File.Exists(changelogFile)) { File.Copy(changelogFile, Path.Combine(buildFolder, "changelog/CHANGELOG.md"), true); } // Generate solution file // This is necessary to have proper cross linking across namespaces. // Otherwise, the links in NamespaceA.ClassA.MemberA to NamespaceB.ClassB will not be clickable in the generated doc // It also allows having preprocessor defines for code generation var constantsDefines = "PACKAGE_DOCS_GENERATION;"; var docToolsConfigFile = Path.Combine(packageDocs, "config.json"); var docToolsConfig = new DocToolsBuildConfig(); if (File.Exists(docToolsConfigFile)) { docToolsConfig = JsonUtility.FromJson <DocToolsBuildConfig>(File.ReadAllText(docToolsConfigFile)); } var configInManual = Path.Combine(manualSource, "config.json"); if (File.Exists(configInManual)) { File.Delete(configInManual); } docToolsConfig.DefineConstants = constantsDefines + docToolsConfig.DefineConstants; var solutionPrefix = "<Project ToolsVersion=\"4.0\" DefaultTargets=\"FullPublish\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\"><PropertyGroup><DefineConstants>" + docToolsConfig.DefineConstants + "</DefineConstants></PropertyGroup><ItemGroup>"; var solutionSuffix = "</ItemGroup></Project>"; var solutionItems = ""; foreach (string cs in Directory.GetFiles(packageCloneFolder, "*.cs", SearchOption.AllDirectories)) { solutionItems += string.Format("<Compile Include=\"{0}\"/>", cs); } var solution = string.Format("{0}\n{1}\n{2}", solutionPrefix, solutionItems, solutionSuffix); File.WriteAllText(Path.Combine(packageCloneFolder, "solution.csproj"), solution); // // Build the doc string arguments = string.Format("\"{0}\"", docfxJson); var success = RunDocFx(arguments); // Copy svg logo if (success) { FileUtil.MoveFileOrDirectory(Path.Combine(buildFolder, "_site"), siteFolder); File.Copy(logoSource, logoTarget, true); // Clear build folder (unless in internal mode for easier debugging) if (!Unsupported.IsDeveloperMode()) { FileUtil.DeleteFileOrDirectory(buildFolder); } } }
private bool RunDocFx(string docFxArguments, bool serveMode = false) { // // Build using docfx Process docfxProcess = new Process(); var startInfo = docfxProcess.StartInfo; if (serveMode) { serveProcess = docfxProcess; } if (!Directory.Exists(DocFxRoot)) { ZipUtils.Unzip(DocFxZip, DocFxRoot); } var applicationExec = MonoPath; string arguments = string.Format("\"{0}\" {1}", DocFxExecutable, docFxArguments); if (Application.platform == RuntimePlatform.WindowsEditor) { applicationExec = DocFxExecutable; arguments = docFxArguments; } string processLog = ""; var success = true; try { startInfo.UseShellExecute = false; startInfo.FileName = applicationExec; startInfo.Arguments = arguments; startInfo.WorkingDirectory = DocFxTemplateRoot; startInfo.CreateNoWindow = true; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; if (serveMode) { startInfo.RedirectStandardInput = true; } // Set our event handler to asynchronously read the sort output. docfxProcess.OutputDataReceived += DocFxOutputHandler; var startMessage = string.Format("DocFx Command line ran: \"{0}\" {1}", applicationExec, arguments); if (GlobalSettings.Verbose) { if (serveMode) { serveLog += "Doc Fx Serve Output: " + startMessage + "\n"; } else { Debug.Log(startMessage); } } docfxProcess.Start(); docfxProcess.BeginOutputReadLine(); // To avoid deadlocks, always read the output stream first and then wait. // Also don't read to end both standard outputs. processLog += docfxProcess.StandardError.ReadToEnd(); if (!serveMode) { docfxProcess.WaitForExit(); } } catch (ThreadAbortException) {} // Don't consider this an error. catch (Exception e) { success = false; Debug.LogError(e.Message); } if (docfxProcess.HasExited && docfxProcess.ExitCode != 0) { success = false; } var message = string.Format("DocFx output:\n{0}", processLog); if (GlobalSettings.Verbose && !string.IsNullOrEmpty(processLog)) { if (serveMode) { serveLog += "Doc Fx Serve Output: " + message + "\n"; } else { Debug.Log(message); } } stopServeLogWatch = true; if (!success) { Debug.LogError(message); } return(success); }