diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 782d4c06f..981d56ebf 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "fornax": { - "version": "0.13.1", + "version": "0.16.0-beta002", "commands": [ "fornax" ], diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..96c07a339 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,17 @@ +# [Choice] Debian version (use bullseye on local arm64/Apple Silicon): bookworm, bullseye, buster +ARG VARIANT="bookworm" +FROM buildpack-deps:${VARIANT}-curl + + +ENV \ + # Enable detection of running in a container + DOTNET_RUNNING_IN_CONTAINER=true \ + DOTNET_ROOT=/usr/share/dotnet/ \ + DOTNET_NOLOGO=true \ + DOTNET_CLI_TELEMETRY_OPTOUT=false\ + DOTNET_USE_POLLING_FILE_WATCHER=true + + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..8db0410a6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,71 @@ +{ + "name": "dotnet", + // Set the build context one level higher so we can grab metadata like global.json + "context": "..", + "dockerFile": "Dockerfile", + "forwardPorts": [ + 0 + ], + "features": { + // https://github.com/devcontainers/features/blob/main/src/common-utils/README.md + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": true, + "installOhMyZshConfig": true, + "configureZshAsDefaultShell": true, + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": true + }, + // https://github.com/devcontainers/features/blob/main/src/github-cli/README.md + "ghcr.io/devcontainers/features/github-cli:1": {}, + // https://github.com/devcontainers-contrib/features/blob/main/src/starship/README.md + "ghcr.io/devcontainers-contrib/features/starship:1": {}, + // https://github.com/devcontainers/features/blob/main/src/dotnet/README.md + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "9.0.201" + } + }, + "overrideFeatureInstallOrder": [ + "ghcr.io/devcontainers/features/common-utils", + "ghcr.io/devcontainers/features/github-cli", + "ghcr.io/devcontainers-contrib/features/starship", + "ghcr.io/devcontainers/features/dotnet" + ], + "customizations": { + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-dotnettools.csharp", + "Ionide.Ionide-fsharp", + "tintoy.msbuild-project-tools", + "ionide.ionide-paket", + "usernamehw.errorlens", + "alefragnani.Bookmarks", + "oderwat.indent-rainbow", + "vscode-icons-team.vscode-icons", + "EditorConfig.EditorConfig", + "GitHub.vscode-pull-request-github", + "github.vscode-github-actions" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "zsh", + "csharp.suppressDotnetInstallWarning": true + } + } + }, + "remoteUser": "vscode", + "containerUser": "vscode", + "containerEnv": { + // Expose the local environment variable to the container + // They are used for releasing and publishing from the container + "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}" + }, + "onCreateCommand": { + "enable-starship": "echo 'eval \"$(starship init zsh)\"' >> ~/.zshrc" + }, + "postAttachCommand": { + "restore": "dotnet tool restore && dotnet restore" + }, + "waitFor": "updateContentCommand" +} diff --git a/.github/workflows/build+test+deploy.yml b/.github/workflows/build+test+deploy.yml index 8471e2b80..fee752f38 100644 --- a/.github/workflows/build+test+deploy.yml +++ b/.github/workflows/build+test+deploy.yml @@ -1,27 +1,18 @@ -name: CI +name: Build, Test and SelfCheck on: - pull_request: - workflow_dispatch: - - # to execute once a day (more info see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule ) - schedule: - - cron: '0 0 * * *' - push: - tags: - - 'v*' + branches: + - master + pull_request: branches: - '**' -env: - DOTNET_VERSION: 9.0.201 - jobs: - buildAndTest: - + build: strategy: matrix: + configuration: [Debug, Release] os: - ubuntu-latest - windows-latest @@ -30,100 +21,48 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET + - name: Setup necessary dotnet SDKs uses: actions/setup-dotnet@v4 with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Restore tools - run: dotnet tool restore - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet fsi build.fsx -t Build - - name: Run tests - run: dotnet fsi build.fsx -t Test - - name: Run FSharpLint on itself - run: dotnet fsi build.fsx -t SelfCheck + global-json-file: global.json + - name: Build via Bash + if: runner.os != 'Windows' + run: ./build.sh + env: + CI: true + CONFIGURATION: ${{ matrix.configuration }} + - name: Build via Windows + if: runner.os == 'Windows' + run: ./build.cmd + env: + CI: true + CONFIGURATION: ${{ matrix.configuration }} - deployReleaseBinaries: - needs: buildAndTest + # Run SelfCheck once on Linux to check for lint warnings + selfcheck: runs-on: ubuntu-latest - + needs: build steps: - uses: actions/checkout@v4 - - name: Setup .NET + - name: Setup necessary dotnet SDKs uses: actions/setup-dotnet@v4 with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Restore tools - run: dotnet tool restore - - name: Build - run: dotnet fsi build.fsx - - name: Pack - run: dotnet fsi build.fsx -t Pack - - name: Get Changelog Entry - id: changelog_reader - uses: mindsers/changelog-reader-action@v1 - with: - version: ${{ github.ref }} - path: ./CHANGELOG.md - - name: Upload binaries to nuget (if nugetKey present) - env: - nuget-key: ${{ secrets.NUGET_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: dotnet fsi build.fsx -t Push - - name: Create Release (if tag) - if: startsWith(github.ref, 'refs/tags/') - id: create_release - uses: actions/create-release@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - body: ${{ steps.changelog_reader.outputs.log_entry }} - draft: false - prerelease: false - - name: Upload binaries to release (if tag) - if: startsWith(github.ref, 'refs/tags/') - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: out/*.nupkg - tag: ${{ github.ref }} - overwrite: true - file_glob: true + global-json-file: global.json + - name: Run SelfCheck + run: ./build.sh SelfCheck + env: + CI: true - deployReleaseDocs: - needs: deployReleaseBinaries + # Builds the project in a dev container + build-devcontainer: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - # old .NET required by old fornax version - - name: Setup old .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 3.1.x + - uses: actions/checkout@v4 - - name: Restore tools - run: dotnet tool restore - - name: Restore dependencies - run: dotnet restore - - name: Run Fornax - run: dotnet fsi build.fsx -t Docs - - name: Deploy (if tag) - if: startsWith(github.ref, 'refs/tags/') - uses: peaceiris/actions-gh-pages@v3 + - name: Build and run dev container task + uses: devcontainers/ci@v0.3 with: - personal_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./docs/_public - publish_branch: gh-pages - force_orphan: true + runCmd: ./build.sh diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 000000000..573922061 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,34 @@ +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: windows-2025 + + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. + permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. + contents: read + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup necessary dotnet SDKs + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json diff --git a/.github/workflows/fornax-gh-pages.yml b/.github/workflows/fornax-gh-pages.yml new file mode 100644 index 000000000..45c0cd709 --- /dev/null +++ b/.github/workflows/fornax-gh-pages.yml @@ -0,0 +1,57 @@ +name: Deploy Docs + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Setup necessary dotnet SDKs + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Build Docs + run: ./build.sh builddocs + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/_public + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish_ci.yml b/.github/workflows/publish_ci.yml new file mode 100644 index 000000000..398c3b5bd --- /dev/null +++ b/.github/workflows/publish_ci.yml @@ -0,0 +1,33 @@ +name: Publish to GitHub + +on: + push: + branches: + - master + +env: + CONFIGURATION: Release + +jobs: + build: + # Sets permissions of the GITHUB_TOKEN to allow release creating + permissions: + packages: write + environment: + name: nuget + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup necessary dotnet SDKs + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Publish to GitHub + env: + NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FAKE_DETAILED_ERRORS: true + run: ./build.sh "PublishToGitHub" diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 000000000..b6bc8636d --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,31 @@ +name: Publish to NuGet + +on: + push: + tags: + - 'v*' + +env: + CONFIGURATION: Release +jobs: + build: + # Sets permissions of the GITHUB_TOKEN to allow release creating + permissions: + contents: write + environment: + name: nuget + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup necessary dotnet SDKs + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Publish to NuGet + env: + NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FAKE_DETAILED_ERRORS: true + ENABLE_COVERAGE: false # AltCover doesn't work with Release builds, reports lower coverage than actual + run: ./build.sh Publish diff --git a/.gitignore b/.gitignore index 04d328f31..ae1bb7787 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ -build/ bld/ [Bb]in/ [Oo]bj/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..64c864b06 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ionide.ionide-fsharp", + "ionide.ionide-fake", + "ms-dotnettools.csharp", + "editorConfig.editorConfig" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..b98926a06 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "FSharp.fsacRuntime":"netcore", + "FSharp.enableAnalyzers": false, + "FSharp.analyzersPath": [ + "./packages/analyzers" + ] +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 9a5a76174..0a183cb5f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,7 +3,7 @@ true - + @@ -14,16 +14,34 @@ - + + + + + + + + + + + + + + + + + + + + - - + \ No newline at end of file diff --git a/FSharpLint.slnf b/FSharpLint.slnf new file mode 100644 index 000000000..1fe23bff1 --- /dev/null +++ b/FSharpLint.slnf @@ -0,0 +1,13 @@ +{ + "solution": { + "path": "FSharpLint.slnx", + "projects": [ + "src\\FSharpLint.Console\\FSharpLint.Console.fsproj", + "src\\FSharpLint.Core\\FSharpLint.Core.fsproj", + "tests\\FSharpLint.Benchmarks\\FSharpLint.Benchmarks.fsproj", + "tests\\FSharpLint.Console.Tests\\FSharpLint.Console.Tests.fsproj", + "tests\\FSharpLint.Core.Tests\\FSharpLint.Core.Tests.fsproj", + "tests\\FSharpLint.FunctionalTest\\FSharpLint.FunctionalTest.fsproj" + ] + } +} \ No newline at end of file diff --git a/FSharpLint.slnx b/FSharpLint.slnx index 2d3d31087..1bc8d8250 100644 --- a/FSharpLint.slnx +++ b/FSharpLint.slnx @@ -5,7 +5,6 @@ - @@ -105,4 +104,5 @@ + diff --git a/build.cmd b/build.cmd new file mode 100644 index 000000000..abc47176f --- /dev/null +++ b/build.cmd @@ -0,0 +1 @@ +dotnet run --project ./build/build.fsproj -- --target %* diff --git a/build.fsx b/build.fsx deleted file mode 100644 index 129fc2c09..000000000 --- a/build.fsx +++ /dev/null @@ -1,279 +0,0 @@ -// -------------------------------------------------------------------------------------- -// FAKE build script -// -------------------------------------------------------------------------------------- -#r "nuget: MSBuild.StructuredLogger" -#r "nuget: Fake.Core" -#r "nuget: Fake.Core.Target" -#r "nuget: Fake.Core.Process" -#r "nuget: Fake.DotNet.Cli" -#r "nuget: Fake.Core.ReleaseNotes" -#r "nuget: Fake.DotNet.AssemblyInfoFile" -#r "nuget: Fake.Tools.Git" -#r "nuget: Fake.Core.Environment" -#r "nuget: Fake.Core.UserInput" -#r "nuget: Fake.IO.FileSystem" -#r "nuget: Fake.DotNet.MsBuild" -#r "nuget: Fake.Api.GitHub" - -#if FAKE -#load ".fake/build.fsx/intellisense.fsx" -#else -// Boilerplate -System.Environment.GetCommandLineArgs() -|> Array.skip 2 // skip fsi.exe; build.fsx -|> Array.toList -|> Fake.Core.Context.FakeExecutionContext.Create false __SOURCE_FILE__ -|> Fake.Core.Context.RuntimeContext.Fake -|> Fake.Core.Context.setExecutionContext - -#endif - -open Fake.Core -open Fake.DotNet -open Fake.Tools -open Fake.IO -open Fake.IO.FileSystemOperators -open Fake.IO.Globbing.Operators -open Fake.Core.TargetOperators -open Fake.Api - -open System -open System.IO - -Target.initEnvironment() - -// -------------------------------------------------------------------------------------- -// Information about the project to be used at NuGet and in AssemblyInfo files -// -------------------------------------------------------------------------------------- - -let project = "FSharpLint" -let solutionFileName = "FSharpLint.slnx" - -let authors = "Matthew Mcveigh" - -let gitOwner = "fsprojects" -let gitName = "FSharpLint" -let gitHome = $"https://github.com/{gitOwner}" -let gitUrl = $"{gitHome}/{gitName}" - -// -------------------------------------------------------------------------------------- -// Helpers -// -------------------------------------------------------------------------------------- -let isNullOrWhiteSpace = System.String.IsNullOrWhiteSpace - -let exec cmd args dir = - let proc = - CreateProcess.fromRawCommandLine cmd args - |> CreateProcess.ensureExitCodeWithMessage $"Error while running '%s{cmd}' with args: %s{args}" - (if isNullOrWhiteSpace dir then proc - else proc |> CreateProcess.withWorkingDirectory dir) - |> Proc.run - |> ignore - -let getBuildParam var = - let value = Environment.environVar var - if String.IsNullOrWhiteSpace value then - None - else - Some value -let DoNothing = ignore - -// -------------------------------------------------------------------------------------- -// Build variables -// -------------------------------------------------------------------------------------- - -let buildDir = "./build/" -let nugetDir = "./out/" -let rootDir = __SOURCE_DIRECTORY__ |> DirectoryInfo - -System.Environment.CurrentDirectory <- rootDir.FullName -let changelogFilename = "CHANGELOG.md" -let changelog = Changelog.load changelogFilename - -let githubRef = Environment.GetEnvironmentVariable "GITHUB_REF" -let tagPrefix = "refs/tags/" -let isTag = - if isNull githubRef then - false - else - githubRef.StartsWith tagPrefix - -let nugetVersion = - match (changelog.Unreleased, isTag) with - | (Some _unreleased, true) -> failwith "Shouldn't publish a git tag for changes outside a real release" - | (None, true) -> - changelog.LatestEntry.NuGetVersion - | (_, false) -> - let current = changelog.LatestEntry.NuGetVersion |> SemVer.parse - let bumped = { current with - Patch = current.Patch + 1u - Original = None - PreRelease = None } - let bumpedBaseVersion = string bumped - - let nugetPreRelease = Path.Combine(rootDir.FullName, "nugetPreRelease.fsx") - let procResult = - CreateProcess.fromRawCommand - "dotnet" - [ - "fsi" - nugetPreRelease - bumpedBaseVersion - ] - |> CreateProcess.redirectOutput - |> CreateProcess.ensureExitCode - |> Proc.run - procResult.Result.Output.Trim() - -let PackageReleaseNotes baseProps = - if isTag then - ("PackageReleaseNotes", $"%s{gitUrl}/blob/v%s{nugetVersion}/CHANGELOG.md")::baseProps - else - baseProps - -// -------------------------------------------------------------------------------------- -// Build Targets -// -------------------------------------------------------------------------------------- - -Target.create "Clean" (fun _ -> - Shell.cleanDirs [buildDir; nugetDir] -) - -Target.create "Build" (fun _ -> - DotNet.build id solutionFileName -) - -let filterPerformanceTests (p:DotNet.TestOptions) = { p with Filter = Some "\"TestCategory!=Performance\""; Configuration = DotNet.Release } - -Target.create "Test" (fun _ -> - DotNet.test filterPerformanceTests "tests/FSharpLint.Core.Tests" - DotNet.test filterPerformanceTests "tests/FSharpLint.Console.Tests" - DotNet.restore id "tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.sln" - DotNet.test filterPerformanceTests "tests/FSharpLint.FunctionalTest" -) - -Target.create "Docs" (fun _ -> - exec "dotnet" @"fornax build" "docs" -) - -// -------------------------------------------------------------------------------------- -// Release Targets -// -------------------------------------------------------------------------------------- - -Target.create "BuildRelease" (fun _ -> - let properties = ("Version", nugetVersion) |> List.singleton |> PackageReleaseNotes - - DotNet.build (fun p -> - { p with - Configuration = DotNet.BuildConfiguration.Release - OutputPath = Some buildDir - MSBuildParams = { p.MSBuildParams with Properties = properties } - } - ) solutionFileName -) - - -Target.create "Pack" (fun _ -> - let properties = PackageReleaseNotes ([ - ("Version", nugetVersion); - ("Authors", authors) - ("PackageProjectUrl", gitUrl) - ("RepositoryType", "git") - ("RepositoryUrl", gitUrl) - ("PackageLicenseExpression", "MIT") - ]) - - DotNet.pack (fun p -> - { p with - Configuration = DotNet.BuildConfiguration.Release - OutputPath = Some nugetDir - MSBuildParams = { p.MSBuildParams with Properties = properties } - } - ) solutionFileName -) - -Target.create "Push" (fun _ -> - let push key = - let distGlob = nugetDir "*.nupkg" - distGlob - |> DotNet.nugetPush (fun o -> { - o with - Common = { - o.Common with - CustomParams = Some "--skip-duplicate" - } - PushParams = { - o.PushParams with - Source = Some "https://api.nuget.org/v3/index.json" - ApiKey = Some key - } - }) - - let key = getBuildParam "nuget-key" - match getBuildParam "GITHUB_EVENT_NAME" with - | None -> - match key with - | None -> - let key = UserInput.getUserPassword "NuGet Key: " - push key - | Some key -> - push key - - | Some "push" -> - match key with - | None -> - Console.WriteLine "No nuget-key env var found, skipping..." - | Some key -> - if isTag then - push key - elif getBuildParam "GITHUB_REF_NAME" <> Some "master" then - Console.WriteLine "Not a push to master branch, skipping..." - else - match getBuildParam "GITHUB_SHA" with - | None -> - failwith "GITHUB_SHA should have been populated" - | Some commitHash -> - let gitArgs = $"describe --exact-match --tags %s{commitHash}" - let proc = - CreateProcess.fromRawCommandLine "git" gitArgs - |> Proc.run - if proc.ExitCode <> 0 then - // commit is not a tag, so go ahead pushing a prerelease - push key - else - Console.WriteLine "Commit mapped to a tag, skipping pushing prerelease..." - | _ -> - Console.WriteLine "Github event name not 'push', skipping..." - -) - - -Target.create "SelfCheck" (fun _ -> - let srcDir = Path.Combine(rootDir.FullName, "src") |> DirectoryInfo - - let consoleProj = Path.Combine(srcDir.FullName, "FSharpLint.Console", "FSharpLint.Console.fsproj") |> FileInfo - let sol = Path.Combine(rootDir.FullName, solutionFileName) |> FileInfo - exec "dotnet" $"run lint %s{sol.FullName}" consoleProj.Directory.FullName -) - -// -------------------------------------------------------------------------------------- -// Build order -// -------------------------------------------------------------------------------------- -Target.create "Default" DoNothing -Target.create "Release" DoNothing - -"Clean" - ==> "Build" - ==> "Test" - ==> "Default" - -"Clean" - ==> "BuildRelease" - ==> "Docs" - -"Default" - ==> "Pack" - ==> "Push" - ==> "Release" - -Target.runOrDefaultWithArguments "Default" diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..2226032ab --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +FAKE_DETAILED_ERRORS=true dotnet run --project ./build/build.fsproj -- --target "$@" diff --git a/build/Changelog.fs b/build/Changelog.fs new file mode 100644 index 000000000..84d81d92c --- /dev/null +++ b/build/Changelog.fs @@ -0,0 +1,233 @@ +module Changelog + +open System +open Fake.Core +open Fake.IO + +let isEmptyChange = + function + | Changelog.Change.Added s + | Changelog.Change.Changed s + | Changelog.Change.Deprecated s + | Changelog.Change.Fixed s + | Changelog.Change.Removed s + | Changelog.Change.Security s + | Changelog.Change.Custom (_, s) -> String.IsNullOrWhiteSpace s.CleanedText + +let tagFromVersionNumber versionNumber = $"v%s{versionNumber}" + +let failOnEmptyChangelog (latestEntry : Changelog.ChangelogEntry) = + let isEmpty = + (latestEntry.Changes |> Seq.forall isEmptyChange) + || latestEntry.Changes |> Seq.isEmpty + + if isEmpty then + failwith "No changes in CHANGELOG. Please add your changes under a heading specified in https://keepachangelog.com/" + +let mkLinkReference (newVersion : SemVerInfo) (changelog : Changelog.Changelog) (gitHubRepoUrl : string) = + if changelog.Entries |> List.isEmpty then + // No actual changelog entries yet: link reference will just point to the Git tag + sprintf "[%s]: %s/releases/tag/%s" newVersion.AsString gitHubRepoUrl (tagFromVersionNumber newVersion.AsString) + else + let versionTuple version = (version.Major, version.Minor, version.Patch) + // Changelog entries come already sorted, most-recent first, by the Changelog module + let prevEntry = + changelog.Entries + |> List.skipWhile (fun entry -> + entry.SemVer.PreRelease.IsSome + || versionTuple entry.SemVer = versionTuple newVersion + ) + |> List.tryHead + + let linkTarget = + match prevEntry with + | Some entry -> + sprintf + "%s/compare/%s...%s" + gitHubRepoUrl + (tagFromVersionNumber entry.SemVer.AsString) + (tagFromVersionNumber newVersion.AsString) + | None -> sprintf "%s/releases/tag/%s" gitHubRepoUrl (tagFromVersionNumber newVersion.AsString) + + sprintf "[%s]: %s" newVersion.AsString linkTarget + +let mkReleaseNotes changelog (latestEntry : Changelog.ChangelogEntry) gitHubRepoUrl = + let linkReference = mkLinkReference latestEntry.SemVer changelog gitHubRepoUrl + + if String.isNullOrEmpty linkReference then + latestEntry.ToString () + else + // Add link reference target to description before building release notes, since in main changelog file it's at the bottom of the file + let description = + match latestEntry.Description with + | None -> linkReference + | Some desc when desc.Contains (linkReference) -> desc + | Some desc -> sprintf "%s\n\n%s" (desc.Trim ()) linkReference + + { latestEntry with Description = Some description }.ToString () + +let getVersionNumber envVarName ctx = + let args = ctx.Context.Arguments + + let verArg = + args + |> List.tryHead + |> Option.defaultWith (fun () -> Environment.environVarOrDefault envVarName "") + + if SemVer.isValid verArg then + verArg + elif verArg.StartsWith ("v") && SemVer.isValid verArg.[1..] then + let target = ctx.Context.FinalTarget + + Trace.traceImportantfn + "Please specify a version number without leading 'v' next time, e.g. \"./build.sh %s %s\" rather than \"./build.sh %s %s\"" + target + verArg.[1..] + target + verArg + + verArg.[1..] + elif String.isNullOrEmpty verArg then + let target = ctx.Context.FinalTarget + + Trace.traceErrorfn + "Please specify a version number, either at the command line (\"./build.sh %s 1.0.0\") or in the %s environment variable" + target + envVarName + + failwith "No version number found" + else + Trace.traceErrorfn "Please specify a valid version number: %A could not be recognized as a version number" verArg + + failwith "Invalid version number" + +let mutable changelogBackupFilename = "" + +let updateChangelog changelogPath (changelog : Fake.Core.Changelog.Changelog) gitHubRepoUrl ctx = + + let verStr = ctx |> getVersionNumber "RELEASE_VERSION" + + let description, unreleasedChanges = + match changelog.Unreleased with + | None -> None, [] + | Some u -> u.Description, u.Changes + + let newVersion = SemVer.parse verStr + + changelog.Entries + |> List.tryFind (fun entry -> entry.SemVer = newVersion) + |> Option.iter (fun entry -> + Trace.traceErrorfn + "Version %s already exists in %s, released on %s" + verStr + changelogPath + (if entry.Date.IsSome then + entry.Date.Value.ToString ("yyyy-MM-dd") + else + "(no date specified)") + + failwith "Can't release with a duplicate version number" + ) + + changelog.Entries + |> List.tryFind (fun entry -> entry.SemVer > newVersion) + |> Option.iter (fun entry -> + Trace.traceErrorfn + "You're trying to release version %s, but a later version %s already exists, released on %s" + verStr + entry.SemVer.AsString + (if entry.Date.IsSome then + entry.Date.Value.ToString ("yyyy-MM-dd") + else + "(no date specified)") + + failwith "Can't release with a version number older than an existing release" + ) + + let versionTuple version = (version.Major, version.Minor, version.Patch) + + let prereleaseEntries = + changelog.Entries + |> List.filter (fun entry -> + entry.SemVer.PreRelease.IsSome + && versionTuple entry.SemVer = versionTuple newVersion + ) + + let prereleaseChanges = + prereleaseEntries + |> List.collect (fun entry -> entry.Changes |> List.filter (not << isEmptyChange)) + |> List.distinct + + let assemblyVersion, nugetVersion = Changelog.parseVersions newVersion.AsString + + let newEntry = + Changelog.ChangelogEntry.New ( + assemblyVersion.Value, + nugetVersion.Value, + Some System.DateTime.Today, + description, + unreleasedChanges @ prereleaseChanges, + false + ) + + let newChangelog = + Changelog.Changelog.New (changelog.Header, changelog.Description, None, newEntry :: changelog.Entries) + + // Save changelog to temporary file before making any edits + changelogBackupFilename <- System.IO.Path.GetTempFileName () + + changelogPath |> Shell.copyFile changelogBackupFilename + + Target.activateFinal "DeleteChangelogBackupFile" + + newChangelog |> Changelog.save changelogPath + + // Now update the link references at the end of the file + let linkReferenceForLatestEntry = mkLinkReference newVersion changelog gitHubRepoUrl + + let linkReferenceForUnreleased = + sprintf "[Unreleased]: %s/compare/%s...%s" gitHubRepoUrl (tagFromVersionNumber newVersion.AsString) "HEAD" + + let tailLines = File.read changelogPath |> List.ofSeq |> List.rev + + let isRef (line : string) = + System.Text.RegularExpressions.Regex.IsMatch (line, @"^\[.+?\]:\s?[a-z]+://.*$") + + let linkReferenceTargets = + tailLines + |> List.skipWhile String.isNullOrWhiteSpace + |> List.takeWhile isRef + |> List.rev // Now most recent entry is at the head of the list + + let newLinkReferenceTargets = + match linkReferenceTargets with + | [] -> [ linkReferenceForUnreleased; linkReferenceForLatestEntry ] + | first :: rest when first |> String.startsWith "[Unreleased]:" -> + linkReferenceForUnreleased + :: linkReferenceForLatestEntry + :: rest + | first :: rest -> + linkReferenceForUnreleased + :: linkReferenceForLatestEntry + :: first + :: rest + + let blankLineCount = + tailLines + |> Seq.takeWhile String.isNullOrWhiteSpace + |> Seq.length + + let linkRefCount = linkReferenceTargets |> List.length + + let skipCount = blankLineCount + linkRefCount + + let updatedLines = + List.rev (tailLines |> List.skip skipCount) + @ newLinkReferenceTargets + + File.write false changelogPath updatedLines + + // If build fails after this point but before we commit changes, undo our modifications + Target.activateBuildFailure "RevertChangelog" + + newEntry diff --git a/build/Fornax.fs b/build/Fornax.fs new file mode 100644 index 000000000..996eb50a4 --- /dev/null +++ b/build/Fornax.fs @@ -0,0 +1,139 @@ +namespace Fake.DotNet + +open System.IO +open Fake.Core +open Fake.IO +open Fake.IO.FileSystemOperators + +/// +/// Contains tasks to interact with Fornax static site generator +/// for F# documentation generation. +/// +[] +module Fornax = + + /// + /// Fornax build command parameters and options + /// + type BuildParams = { + /// Working directory to run Fornax from (default: docs directory) + WorkingDirectory : string option + + /// Timeout for the build process + Timeout : System.TimeSpan option + + /// Whether to fail if Fornax returns non-zero exit code + FailOnError : bool + } with + /// Parameter default values. + static member Default = { + WorkingDirectory = None + Timeout = None + FailOnError = true + } + + /// + /// Fornax watch command parameters and options + /// + type WatchParams = { + /// Working directory to run Fornax from (default: docs directory) + WorkingDirectory : string option + + /// Port to serve content on (default: 8080) + Port : int option + + /// Whether to fail if Fornax returns non-zero exit code + FailOnError : bool + } with + /// Parameter default values. + static member Default = { + WorkingDirectory = None + Port = None + FailOnError = true + } + + /// + /// Build documentation using Fornax + /// + /// + /// Function used to overwrite the build command default parameters. + /// + /// + /// + /// Fornax.build (fun p -> { p with WorkingDirectory = Some "./docs" }) + /// + /// + let build setBuildParams = + let buildParams = setBuildParams BuildParams.Default + + let processArgs = + CreateProcess.fromRawCommandLine "dotnet" "fornax build" + |> CreateProcess.withTimeout (buildParams.Timeout |> Option.defaultValue (System.TimeSpan.FromMinutes 10.0)) + |> (fun args -> + match buildParams.WorkingDirectory with + | Some dir -> CreateProcess.withWorkingDirectory dir args + | None -> args) + |> (fun args -> + if buildParams.FailOnError then + CreateProcess.ensureExitCode args + else + args) + + let result = processArgs |> Proc.run + + if buildParams.FailOnError && result.ExitCode <> 0 then + failwithf "Fornax build failed with exit code %d" result.ExitCode + else + result + + /// + /// Watch documentation using Fornax with hot reload + /// + /// + /// Function used to overwrite the watch command default parameters. + /// + /// + /// + /// Fornax.watch (fun p -> { p with Port = Some 3000; WorkingDirectory = Some "./docs" }) + /// + /// + let watch setWatchParams = + let watchParams = setWatchParams WatchParams.Default + + let args = + match watchParams.Port with + | Some port -> $"fornax watch --port {port}" + | None -> "fornax watch" + + let processArgs = + CreateProcess.fromRawCommandLine "dotnet" args + |> (fun args -> + match watchParams.WorkingDirectory with + | Some dir -> CreateProcess.withWorkingDirectory dir args + | None -> args) + |> (fun args -> + if watchParams.FailOnError then + CreateProcess.ensureExitCode args + else + args) + + let result = processArgs |> Proc.run + + if watchParams.FailOnError && result.ExitCode <> 0 then + failwithf "Fornax watch failed with exit code %d" result.ExitCode + else + result + + /// + /// Clean Fornax cache and generated files + /// + /// + /// Working directory where Fornax cache should be cleaned + let cleanCache workingDirectory = + let cacheDir = workingDirectory "_public" + let tempDir = workingDirectory "_temp" + + if Directory.Exists cacheDir then + Shell.cleanDir cacheDir + if Directory.Exists tempDir then + Shell.cleanDir tempDir diff --git a/build/Properties/launchSettings.json b/build/Properties/launchSettings.json new file mode 100644 index 000000000..5b2353b61 --- /dev/null +++ b/build/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "profiles": { + "BuildAndTest": { + "commandName": "Project", + "commandLineArgs": "--target DotnetTest" + }, + "Publish": { + "commandName": "Project", + "commandLineArgs": "--target Publish" + }, + "PublishToGitHub": { + "commandName": "Project", + "commandLineArgs": "--target PublishToGitHub" + }, + "BuildDocs": { + "commandName": "Project", + "commandLineArgs": "--target BuildDocs" + }, + "SelfCheck": { + "commandName": "Project", + "commandLineArgs": "--target SelfCheck" + }, + "Release": { + "commandName": "Project", + "commandLineArgs": "--target Release 0.24.2" + } + } +} diff --git a/build/build.fs b/build/build.fs new file mode 100644 index 000000000..c2d475570 --- /dev/null +++ b/build/build.fs @@ -0,0 +1,767 @@ +open System +open System.Xml.Linq +open Fake.Core +open Fake.DotNet +open Fake.Tools +open Fake.IO +open Fake.IO.FileSystemOperators +open Fake.IO.Globbing.Operators +open Fake.Core.TargetOperators +open Fake.Api +open Fake.BuildServer +open Argu + +let environVarAsBoolOrDefault varName defaultValue = + let truthyConsts = [ "1"; "Y"; "YES"; "T"; "TRUE" ] + Environment.environVar varName + |> ValueOption.ofObj + |> ValueOption.map (fun envvar -> + truthyConsts + |> List.exists (fun ``const`` -> String.Equals (``const``, envvar, StringComparison.InvariantCultureIgnoreCase)) + ) + |> ValueOption.defaultValue defaultValue + +//----------------------------------------------------------------------------- +// Metadata and Configuration +//----------------------------------------------------------------------------- + +let rootDirectory = __SOURCE_DIRECTORY__ ".." + +let productName = "FSharpLint" + +let sln = rootDirectory "FSharpLint.slnf" + +let srcCodeGlob = + !!(rootDirectory "src/**/*.fs") + ++ (rootDirectory "src/**/*.fsx") + -- (rootDirectory "src/**/obj/**/*.fs") + +let testsCodeGlob = + !!(rootDirectory "tests/**/*.fs") + ++ (rootDirectory "tests/**/*.fsx") + -- (rootDirectory "tests/**/obj/**/*.fs") + +let srcGlob = rootDirectory "src/**/*.??proj" + +let testsGlob = rootDirectory "tests/**/*.??proj" + +let srcAndTest = !!srcGlob ++ testsGlob + +let distDir = rootDirectory "dist" + +let distGlob = distDir "*.nupkg" + +let docsDir = rootDirectory "docs" + +let docsSrcDir = rootDirectory "docsSrc" + +let temp = rootDirectory "temp" + +let watchDocsDir = temp "watch-docs" + +let gitOwner = "fsprojects" +let gitRepoName = "FSharpLint" + +let gitHubRepoUrl = $"https://github.com/%s{gitOwner}/%s{gitRepoName}" + +let documentationRootUrl = $"https://%s{gitOwner}.github.io/%s{gitRepoName}" + +let releaseBranch = "main" +let readme = "README.md" +let changelogFile = "CHANGELOG.md" + +// fsharplint:disable FL0046 +let READMElink = Uri (Uri (gitHubRepoUrl), $"blob/{releaseBranch}/{readme}") +let CHANGELOGlink = Uri (Uri (gitHubRepoUrl), $"blob/{releaseBranch}/{changelogFile}") +// fsharplint:enable FL0046 + +let changelogPath = rootDirectory changelogFile + +let changelog = Fake.Core.Changelog.load changelogPath + +let mutable latestEntry = + if Seq.isEmpty changelog.Entries then + Changelog.ChangelogEntry.New ("0.0.1", "0.0.1-alpha.1", Some DateTime.Today, None, [], false) + else + changelog.LatestEntry + +let mutable changelogBackupFilename = "" + +let publishUrl = "https://www.nuget.org" + +let githubToken = Environment.environVarOrNone "GITHUB_TOKEN" + +let nugetToken = Environment.environVarOrNone "NUGET_KEY" + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +let isRelease (targets : Target list) = + targets + |> Seq.map (fun t -> t.Name) + |> Seq.exists ((=) "PublishToNuGet") + +let invokeAsync f = async { f () } + +let configuration (targets : Target list) = + let defaultVal = if isRelease targets then "Release" else "Debug" + + match Environment.environVarOrDefault "CONFIGURATION" defaultVal with + | "Debug" -> DotNet.BuildConfiguration.Debug + | "Release" -> DotNet.BuildConfiguration.Release + | config -> DotNet.BuildConfiguration.Custom config + +let failOnBadExitAndPrint (p : ProcessResult) = + if p.ExitCode <> 0 then + p.Errors |> Seq.iter Trace.traceError + + failwithf "failed with exitcode %d" p.ExitCode + +let isPublishToGitHub ctx = ctx.Context.FinalTarget = "PublishToGitHub" + +let isCI = lazy environVarAsBoolOrDefault "CI" false + +// CI Servers can have bizarre failures that have nothing to do with your code +let rec retryIfInCI times fn = + match isCI.Value with + | true -> + if times > 1 then + try + fn () + with _ -> + retryIfInCI (times - 1) fn + else + fn () + | _ -> fn () + +let failOnWrongBranch () = + if Git.Information.getBranchName "" <> releaseBranch then + failwithf "Not on %s. If you want to release please switch to this branch." releaseBranch + + +module dotnet = + let watch cmdParam program args = DotNet.exec cmdParam ($"watch %s{program}") args + + let run cmdParam args = DotNet.exec cmdParam "run" args + + let tool optionConfig (command : string) args = + DotNet.exec optionConfig command args + |> failOnBadExitAndPrint + + let sourcelink optionConfig args = tool optionConfig "sourcelink" args + + let fcswatch optionConfig args = tool optionConfig "fcswatch" args + + let fsharpAnalyzer optionConfig args = tool optionConfig "fsharp-analyzers" args + + let fantomas args = DotNet.exec id "fantomas" args + +module FSharpAnalyzers = + // fsharplint:disable FL0041 + type Arguments = + | Project of string + | Analyzers_Path of string + | Fail_On_Warnings of string list + | Ignore_Files of string list + | Verbose + // fsharplint:enable FL0041 + + interface IArgParserTemplate with + member s.Usage = "" + + +module DocsTool = + /// + /// Clean Fornax cache and generated files + /// + let cleanDocsCache () = Fornax.cleanCache docsDir + + /// + /// Build documentation using Fornax + /// + let build (configuration) = + let result = Fornax.build (fun p -> { p with WorkingDirectory = Some docsDir }) + result |> ignore + + /// + /// Watch documentation using Fornax with hot reload + /// + let watch (configuration) = + let result = Fornax.watch (fun p -> { p with WorkingDirectory = Some docsDir }) + result |> ignore + +module NuGetConfig = + /// + /// Add GitHub package source to NuGet configuration + /// + let addGitHubSource () = + let result = + DotNet.exec id "nuget" "add source --name \"github.com\" \"https://nuget.pkg.github.com/fsprojects/index.json\"" + + if not result.OK then + Trace.logf "Warning: Failed to add GitHub source: %A" result.Errors + + /// + /// Ensure NuGet package source mapping configuration + /// + let ensurePackageSourceMapping () = + let homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + let nugetConfigPath = homeDir ".nuget" "NuGet" "NuGet.Config" + + try + if IO.File.Exists nugetConfigPath then + let doc = XDocument.Load(nugetConfigPath) + + match doc.Root with + | null -> + Trace.logf "Warning: Invalid XML structure in %s" nugetConfigPath + | configRoot -> + // Remove existing packageSourceMapping if it exists + configRoot.Element(XName.Get("packageSourceMapping")) + |> ValueOption.ofObj + |> ValueOption.iter (fun existingMapping -> existingMapping.Remove()) + + // Create new packageSourceMapping element + let packageSourceMapping = XElement(XName.Get("packageSourceMapping")) + + // Create nuget.org source mapping + let nugetSource = XElement(XName.Get("packageSource")) + nugetSource.SetAttributeValue(XName.Get("key"), "nuget.org") + + let nugetPattern = XElement(XName.Get("package")) + nugetPattern.SetAttributeValue(XName.Get("pattern"), "*") + + nugetSource.Add(nugetPattern) + packageSourceMapping.Add(nugetSource) + configRoot.Add(packageSourceMapping) + + doc.Save(nugetConfigPath) + Trace.log "Successfully updated NuGet package source mapping" + else + Trace.logf "Warning: NuGet config file not found at %s" nugetConfigPath + with + | ex -> + Trace.logf "Warning: Failed to update NuGet package source mapping: %s" ex.Message + +let allReleaseChecks () = failOnWrongBranch () +//Changelog.failOnEmptyChangelog latestEntry + +let failOnLocalBuild () = + if not isCI.Value then + failwith "Not on CI. If you want to publish, please use CI." + +let failOnCIBuild () = + if isCI.Value then + failwith "On CI. If you want to run this target, please use a local build." + +let allPublishChecks () = failOnLocalBuild () +//Changelog.failOnEmptyChangelog latestEntry + +//----------------------------------------------------------------------------- +// Target Implementations +//----------------------------------------------------------------------------- + +/// So we don't require always being on the latest MSBuild.StructuredLogger +let disableBinLog (p : MSBuild.CliArguments) = { p with DisableInternalBinLog = true } + +let clean _ = + [ "bin"; "temp"; distDir ] + |> Shell.cleanDirs + + !!srcGlob ++ testsGlob + |> Seq.collect (fun p -> + [ "bin"; "obj" ] + |> Seq.map (fun sp -> IO.Path.GetDirectoryName p sp) + ) + |> Shell.cleanDirs + +let dotnetRestore _ = + [ sln ] + |> Seq.map (fun dir -> + fun () -> + let args = [] |> String.concat " " + + DotNet.restore + (fun c -> { + c with + MSBuildParams = disableBinLog c.MSBuildParams + Common = c.Common |> DotNet.Options.withCustomParams (Some (args)) + }) + dir + ) + |> Seq.iter (retryIfInCI 10) + +let dotnetToolRestore _ = + let result = + fun () -> DotNet.exec id "tool" "restore" + |> (retryIfInCI 10) + + if not result.OK then + failwithf "Failed to restore .NET tools: %A" result.Errors + +let updateChangelog ctx = + latestEntry <- + if not <| isPublishToGitHub ctx then + Changelog.updateChangelog changelogPath changelog gitHubRepoUrl ctx + elif Seq.isEmpty changelog.Entries then + latestEntry + else + let latest = changelog.LatestEntry + let semVer = { + latest.SemVer with + Original = None + Patch = latest.SemVer.Patch + 1u + PreRelease = PreRelease.TryParse "ci" + } + { + latest with + SemVer = semVer + NuGetVersion = semVer.AsString + AssemblyVersion = semVer.AsString + } + +let revertChangelog _ = + if String.isNotNullOrEmpty Changelog.changelogBackupFilename then + Changelog.changelogBackupFilename + |> Shell.copyFile changelogPath + +let deleteChangelogBackupFile _ = + if String.isNotNullOrEmpty Changelog.changelogBackupFilename then + Shell.rm Changelog.changelogBackupFilename + +let getPackageVersionProperty publishToGitHub = + if publishToGitHub then + let runId = Environment.environVar "GITHUB_RUN_ID" + $"/p:PackageVersion=%s{latestEntry.NuGetVersion}-%s{runId}" + else + $"/p:PackageVersion=%s{latestEntry.NuGetVersion}" + +let dotnetBuild ctx = + + let publishToGitHub = isPublishToGitHub ctx + + let args = [ getPackageVersionProperty publishToGitHub; "--no-restore" ] + + DotNet.build + (fun c -> { + c with + Configuration = configuration (ctx.Context.AllExecutingTargets) + Common = c.Common |> DotNet.Options.withAdditionalArgs args + MSBuildParams = { + (disableBinLog c.MSBuildParams) with + Properties = [ + if publishToGitHub then + ("DebugType", "embedded") + ("EmbedAllSources", "true") + ] + } + }) + sln + +let fsharpAnalyzers _ = + let argParser = ArgumentParser.Create (programName = "fsharp-analyzers") + + !!srcGlob + |> Seq.iter (fun proj -> + let args = + [ + FSharpAnalyzers.Analyzers_Path (rootDirectory "packages/analyzers") + FSharpAnalyzers.Arguments.Project proj + FSharpAnalyzers.Arguments.Fail_On_Warnings [ "BDH0002" ] + FSharpAnalyzers.Arguments.Ignore_Files [ "*AssemblyInfo.fs" ] + FSharpAnalyzers.Verbose + ] + |> argParser.PrintCommandLineArgumentsFlat + + dotnet.fsharpAnalyzer id args + ) + +let dotnetTest ctx = + let args = [ "--no-build" ] + + // Filter performance tests like in build.fsx + let filterPerformanceTests (p : DotNet.TestOptions) = { + p with + Filter = Some "\"TestCategory!=Performance\"" + Configuration = configuration (ctx.Context.AllExecutingTargets) + } + + // Run the same test projects as in build.fsx + DotNet.test + (filterPerformanceTests + >> fun opts -> { + opts with + MSBuildParams = disableBinLog opts.MSBuildParams + Common = + opts.Common + |> DotNet.Options.withAdditionalArgs args + }) + (rootDirectory "tests/FSharpLint.Core.Tests") + + DotNet.test + (filterPerformanceTests + >> fun opts -> { + opts with + MSBuildParams = disableBinLog opts.MSBuildParams + Common = + opts.Common + |> DotNet.Options.withAdditionalArgs args + }) + (rootDirectory "tests/FSharpLint.Console.Tests") + + // Restore the functional test project like in build.fsx + DotNet.restore id (rootDirectory "tests/FSharpLint.FunctionalTest.TestedProject/FSharpLint.FunctionalTest.TestedProject.sln") + + DotNet.test + (filterPerformanceTests + >> fun opts -> { + opts with + MSBuildParams = disableBinLog opts.MSBuildParams + Common = + opts.Common + |> DotNet.Options.withAdditionalArgs args + }) + (rootDirectory "tests/FSharpLint.FunctionalTest") + +let watchTests _ = + !!testsGlob + |> Seq.map (fun proj -> + fun () -> + dotnet.watch + (fun opt -> + opt + |> DotNet.Options.withWorkingDirectory (IO.Path.GetDirectoryName proj) + ) + "test" + "" + |> ignore + ) + |> Seq.iter (invokeAsync >> Async.Catch >> Async.Ignore >> Async.Start) + + printfn "Press Ctrl+C (or Ctrl+Break) to stop..." + + let cancelEvent = + Console.CancelKeyPress + |> Async.AwaitEvent + |> Async.RunSynchronously + + cancelEvent.Cancel <- true + +let generateAssemblyInfo _ = + + let (|Fsproj|Csproj|Vbproj|) (projFileName : string) = + match projFileName with + | f when f.EndsWith ("fsproj") -> Fsproj + | f when f.EndsWith ("csproj") -> Csproj + | f when f.EndsWith ("vbproj") -> Vbproj + | _ -> failwith $"Project file %s{projFileName} not supported. Unknown project type." + + let releaseChannel = + match latestEntry.SemVer.PreRelease with + | Some pr -> pr.Name + | _ -> "release" + + let getAssemblyInfoAttributes projectName = [ + AssemblyInfo.Title (projectName) + AssemblyInfo.Product productName + AssemblyInfo.Version latestEntry.AssemblyVersion + AssemblyInfo.Metadata ("ReleaseDate", latestEntry.Date.Value.ToString ("o")) + AssemblyInfo.FileVersion latestEntry.AssemblyVersion + AssemblyInfo.InformationalVersion latestEntry.AssemblyVersion + AssemblyInfo.Metadata ("ReleaseChannel", releaseChannel) + AssemblyInfo.Metadata ("GitHash", Git.Information.getCurrentSHA1 (null)) + ] + + let getProjectDetails (projectPath : string) = + let projectName = IO.Path.GetFileNameWithoutExtension (projectPath) + + (projectPath, projectName, IO.Path.GetDirectoryName (projectPath), (getAssemblyInfoAttributes projectName)) + + !!srcGlob + |> Seq.map getProjectDetails + |> Seq.iter (fun (projFileName, _, folderName, attributes) -> + match projFileName with + | Fsproj -> AssemblyInfoFile.createFSharp (folderName "AssemblyInfo.fs") attributes + | Csproj -> AssemblyInfoFile.createCSharp ((folderName "Properties") "AssemblyInfo.cs") attributes + | Vbproj -> AssemblyInfoFile.createVisualBasic ((folderName "My Project") "AssemblyInfo.vb") attributes + ) + +let dotnetPack ctx = + // Get release notes with properly-linked version number + let releaseNotes = Changelog.mkReleaseNotes changelog latestEntry gitHubRepoUrl + + let args = [ getPackageVersionProperty (isPublishToGitHub ctx); $"/p:PackageReleaseNotes=\"{releaseNotes}\"" ] + + DotNet.pack + (fun c -> { + c with + MSBuildParams = disableBinLog c.MSBuildParams + Configuration = configuration (ctx.Context.AllExecutingTargets) + OutputPath = Some distDir + Common = c.Common |> DotNet.Options.withAdditionalArgs args + }) + sln + +let sourceLinkTest _ = + !!distGlob + |> Seq.iter (fun nupkg -> dotnet.sourcelink id $"test %s{nupkg}") + +type PushSource = + | NuGet + | GitHub + +let publishTo (source : PushSource) _ = + allPublishChecks () + + distGlob + |> DotNet.nugetPush (fun o -> { + o with + Common = { + o.Common with + WorkingDirectory = "dist" + CustomParams = Some "--skip-duplicate" + } + PushParams = { + o.PushParams with + NoSymbols = source.IsGitHub + Source = + match source with + | NuGet -> Some "nuget.org" + | GitHub -> Some "github.com" + ApiKey = + match source with + | NuGet -> nugetToken + | GitHub -> githubToken + } + }) + +let gitRelease _ = + allReleaseChecks () + + let releaseNotesGitCommitFormat = latestEntry.ToString () + + Git.Staging.stageFile "" (rootDirectory "CHANGELOG.md") + |> ignore + + !!(rootDirectory "src/**/AssemblyInfo.fs") + ++ (rootDirectory "tests/**/AssemblyInfo.fs") + |> Seq.iter (Git.Staging.stageFile "" >> ignore) + + let msg = $"Bump version to `%s{latestEntry.NuGetVersion}`\n\n%s{releaseNotesGitCommitFormat}" + + Git.Commit.exec "" msg + + Target.deactivateBuildFailure "RevertChangelog" + + Git.Branches.push "" + + let tag = Changelog.tagFromVersionNumber latestEntry.NuGetVersion + + Git.Branches.tag "" tag + Git.Branches.pushTag "" "origin" tag + +let githubRelease _ = + allPublishChecks () + + let token = + match githubToken with + | Some s -> s + | _ -> failwith "please set the `GITHUB_TOKEN` environment variable to a github personal access token with repo access." + + let files = !!distGlob + // Get release notes with properly-linked version number + let releaseNotes = Changelog.mkReleaseNotes changelog latestEntry gitHubRepoUrl + + GitHub.createClientWithToken token + |> GitHub.draftNewRelease + gitOwner + gitRepoName + (Changelog.tagFromVersionNumber latestEntry.NuGetVersion) + (latestEntry.SemVer.PreRelease <> None) + (releaseNotes |> Seq.singleton) + |> GitHub.uploadFiles files + |> GitHub.publishDraft + |> Async.RunSynchronously + +let formatCode _ = + let result = dotnet.fantomas $"{rootDirectory}" + + if not result.OK then + printfn "Errors while formatting all files: %A" result.Messages + +let checkFormatCode ctx = + let result = dotnet.fantomas $"{rootDirectory} --check" + + if result.ExitCode = 0 then + Trace.log "No files need formatting" + elif result.ExitCode = 99 then + failwith "Some files need formatting, check output for more info" + else + Trace.logf "Errors while formatting: %A" result.Errors + + +let cleanDocsCache _ = DocsTool.cleanDocsCache () + +let buildDocs ctx = + let configuration = configuration (ctx.Context.AllExecutingTargets) + + // Build only FSharpLint.Core project for documentation + DotNet.build + (fun c -> { + c with + Configuration = DotNet.BuildConfiguration.fromString (string configuration) + MSBuildParams = disableBinLog c.MSBuildParams + }) + (rootDirectory "src/FSharpLint.Core") + + DocsTool.build (string configuration) + +let watchDocs ctx = + let configuration = configuration (ctx.Context.AllExecutingTargets) + DocsTool.watch (string configuration) + +let configureNuGetForGitHub _ = + Trace.log "Configuring NuGet for GitHub package publishing..." + NuGetConfig.addGitHubSource () + NuGetConfig.ensurePackageSourceMapping () + +let selfCheck _ = + let srcDir = rootDirectory "src" + let consoleProj = srcDir "FSharpLint.Console" + let sol = sln + + DotNet.exec + (fun opts -> { opts with WorkingDirectory = consoleProj }) + "run" + $"lint %s{sol}" + |> failOnBadExitAndPrint + +let initTargets (ctx : Context.FakeExecutionContext) = + BuildServer.install [ GitHubActions.Installer ] + + let isPublishToGitHub = + ctx.Arguments + |> Seq.pairwise + |> Seq.exists (fun (arg, value) -> + (String.Equals (arg, "-t", StringComparison.OrdinalIgnoreCase) + || String.Equals (arg, "--target", StringComparison.OrdinalIgnoreCase)) + && String.Equals (value, "PublishToGitHub", StringComparison.OrdinalIgnoreCase) + ) + + /// Defines a dependency - y is dependent on x. Finishes the chain. + let (==>!) x y = x ==> y |> ignore + + /// Defines a soft dependency. x must run before y, if it is present, but y does not require x to be run. Finishes the chain. + let (?=>!) x y = x ?=> y |> ignore + //----------------------------------------------------------------------------- + // Hide Secrets in Logger + //----------------------------------------------------------------------------- + Option.iter (TraceSecrets.register "") githubToken + Option.iter (TraceSecrets.register "") nugetToken + //----------------------------------------------------------------------------- + // Target Declaration + //----------------------------------------------------------------------------- + + Target.create "Clean" clean + Target.create "DotnetRestore" dotnetRestore + Target.create "DotnetToolRestore" dotnetToolRestore + Target.create "ConfigureNuGetForGitHub" configureNuGetForGitHub + Target.create "UpdateChangelog" updateChangelog + Target.createBuildFailure "RevertChangelog" revertChangelog // Do NOT put this in the dependency chain + Target.createFinal "DeleteChangelogBackupFile" deleteChangelogBackupFile // Do NOT put this in the dependency chain + Target.create "DotnetBuild" dotnetBuild + Target.create "FSharpAnalyzers" fsharpAnalyzers + Target.create "DotnetTest" dotnetTest + Target.create "WatchTests" watchTests + Target.create "GenerateAssemblyInfo" generateAssemblyInfo + Target.create "DotnetPack" dotnetPack + Target.create "SourceLinkTest" sourceLinkTest + Target.create "PublishToNuGet" (publishTo NuGet) + Target.create "PublishToGitHub" (publishTo GitHub) + Target.create "GitRelease" gitRelease + Target.create "GitHubRelease" githubRelease + Target.create "FormatCode" formatCode + Target.create "CheckFormatCode" checkFormatCode + Target.create "Release" ignore // For local + Target.create "Publish" ignore //For CI + Target.create "CleanDocsCache" cleanDocsCache + Target.create "BuildDocs" buildDocs + Target.create "WatchDocs" watchDocs + Target.create "SelfCheck" selfCheck + + //----------------------------------------------------------------------------- + // Target Dependencies + //----------------------------------------------------------------------------- + + // Only call Clean if DotnetPack was in the call chain + // Ensure Clean is called before DotnetRestore + "Clean" ?=>! "DotnetRestore" + + "Clean" ==>! "DotnetPack" + + // Only call GenerateAssemblyInfo if GitRelease was in the call chain + // Ensure GenerateAssemblyInfo is called after DotnetRestore and before DotnetBuild + "DotnetRestore" ?=>! "GenerateAssemblyInfo" + + "GenerateAssemblyInfo" ?=>! "DotnetBuild" + + // Ensure UpdateChangelog is called after DotnetRestore + "DotnetRestore" ?=>! "UpdateChangelog" + + "UpdateChangelog" ?=>! "GenerateAssemblyInfo" + + "CleanDocsCache" ==>! "BuildDocs" + + // BuildDocs doesn't need DotnetBuild as it builds FSharpLint.Core itself + // "DotnetBuild" ?=>! "BuildDocs" + // "DotnetBuild" ==>! "BuildDocs" + + "DotnetBuild" ==>! "WatchDocs" + + "UpdateChangelog" + ==> "GenerateAssemblyInfo" + ==> "GitRelease" + ==>! "Release" + + + "DotnetRestore" =?> ("CheckFormatCode", isCI.Value) + ==> "DotnetBuild" + ==> "DotnetTest" + ==> "DotnetPack" + ==> "PublishToNuGet" + ==> "GitHubRelease" + ==>! "Publish" + + "DotnetRestore" + =?> ("CheckFormatCode", isCI.Value) + =?> ("GenerateAssemblyInfo", isPublishToGitHub) + =?> ("ConfigureNuGetForGitHub", isPublishToGitHub && isCI.Value && githubToken.IsSome) + ==> "DotnetBuild" + ==> "DotnetTest" + ==> "DotnetPack" + ==>! "PublishToGitHub" + + "DotnetRestore" ==>! "WatchTests" + + "DotnetToolRestore" ?=>! "DotnetRestore" + "DotnetToolRestore" ==>! "BuildDocs" + "DotnetToolRestore" ?=>! "CheckFormatCode" + "DotnetToolRestore" ?=>! "FormatCode" + +//----------------------------------------------------------------------------- +// Target Start +//----------------------------------------------------------------------------- +[] +let main argv = + + let ctx = + argv + |> Array.toList + |> Context.FakeExecutionContext.Create false "build.fsx" + + Context.setExecutionContext (Context.RuntimeContext.Fake ctx) + initTargets ctx + Target.runOrDefaultWithArguments "DotnetPack" + + 0 // return an integer exit code diff --git a/build/build.fsproj b/build/build.fsproj new file mode 100644 index 000000000..e5f85cef1 --- /dev/null +++ b/build/build.fsproj @@ -0,0 +1,37 @@ + + + Exe + net9.0 + 3390;$(WarnOn) + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/_lib/Fornax.Core.dll b/docs/_lib/Fornax.Core.dll deleted file mode 100644 index b8754840a..000000000 Binary files a/docs/_lib/Fornax.Core.dll and /dev/null differ diff --git a/docs/config.fsx b/docs/config.fsx index 2e724c98f..571d1b0d0 100644 --- a/docs/config.fsx +++ b/docs/config.fsx @@ -1,4 +1,4 @@ -#r "_lib/Fornax.Core.dll" +#r "nuget: Fornax.Core, 0.15.1" open Config diff --git a/docs/config.fsx.lock b/docs/config.fsx.lock index db3f86c72..4d3cbd809 100644 --- a/docs/config.fsx.lock +++ b/docs/config.fsx.lock @@ -2,4 +2,5 @@ STORAGE: NONE RESTRICTION: == netstandard2.0 NUGET remote: https://api.nuget.org/v3/index.json - FSharp.Core (4.7.2) + FSharp.Compiler.Service (41.0.7) + FSharp.Core (6.0.7) diff --git a/docs/generators/apiref.fsx b/docs/generators/apiref.fsx index 559dbb13d..727bbc234 100644 --- a/docs/generators/apiref.fsx +++ b/docs/generators/apiref.fsx @@ -1,7 +1,6 @@ -#r "../_lib/Fornax.Core.dll" -#r "../../packages/docs/Markdig/lib/netstandard2.0/Markdig.dll" -#r "../../packages/docs/Newtonsoft.Json/lib/netstandard2.0/Newtonsoft.Json.dll" -#r "../../packages/docs/FSharp.Formatting/lib/netstandard2.0/FSharp.MetadataFormat.dll" +#r "nuget: Fornax.Core, 0.15.1" +#r "nuget: Markdig, 0.41.3" +#r "nuget: FSharp.Formatting, 20.0.1" #if !FORNAX #load "../loaders/apirefloader.fsx" @@ -10,7 +9,8 @@ #load "partials/layout.fsx" open System -open FSharp.MetadataFormat +open System.Text.Json +open FSharp.Formatting.ApiDocs open Html open Apirefloader open Markdig @@ -21,7 +21,7 @@ let markdownPipeline = .UseGridTables() .Build() -let getComment (c: Comment) = +let getComment (c: ApiDocComment) = let t = c.RawData |> List.map (fun n -> n.Value) @@ -29,7 +29,7 @@ let getComment (c: Comment) = Markdown.ToHtml(t, markdownPipeline) -let formatMember (m: Member) = +let formatMember (m: ApiDocMember) = let attributes = m.Attributes |> List.filter (fun a -> a.FullName <> "Microsoft.FSharp.Core.CustomOperationAttribute") @@ -62,7 +62,8 @@ let formatMember (m: Member) = br [] br [] b [] [!! "Signature: "] - !!m.Details.Signature + match m.Details with + | ApiDocMemberDetails(usageHtml, _, _, _, _, _, _, _) -> !!usageHtml.HtmlText br [] if not (attributes.IsEmpty) then b [] [!! "Attributes:"] @@ -72,103 +73,117 @@ let formatMember (m: Member) = td [] [!! (getComment m.Comment)] ] -let generateType ctx (page: ApiPageInfo) = +let generateType ctx (page: ApiPageInfo) = let t = page.Info let body = div [Class "api-page"] [ h2 [] [!! t.Name] b [] [!! "Namespace: "] - a [Href ($"%s{page.NamespaceUrlName}.html")] [!! page.NamespaceName] + a [Href ($"../%s{page.NamespaceUrlName}.html")] [!! page.NamespaceName] br [] - b [] [!! "Parent: "] - a [Href ($"%s{page.ParentUrlName}.html")] [!! page.ParentName] - span [] [!! (getComment t.Comment)] + if page.ParentName <> page.NamespaceName then + b [] [!! "Parent Module: "] + a [Href ($"../%s{page.ParentUrlName}.html")] [!! page.ParentName] + br [] + b [] [!! "Assembly: "] + !! t.Assembly.Name br [] if not (String.IsNullOrWhiteSpace t.Category) then - b [] [!! "Category:"] + b [] [!! "Category: "] !!t.Category br [] if not (t.Attributes.IsEmpty) then - b [] [!! "Attributes:"] + b [] [!! "Attributes: "] for a in t.Attributes do - br [] code [] [!! (a.Name)] - br [] + br [] + br [] table [] [ tr [] [ th [ Width "35%" ] [!!"Name"] th [ Width "65%"] [!!"Description"] ] - if not t.Constructors.IsEmpty then tr [] [ td [ColSpan 3. ] [ b [] [!! "Constructors"]]] + if not (t.Constructors : ApiDocMember list).IsEmpty then tr [] [ td [ColSpan 2. ] [ b [] [!! "Constructors"]]] yield! t.Constructors |> List.map formatMember - if not t.InstanceMembers.IsEmpty then tr [] [ td [ColSpan 3. ] [ b [] [!! "Instance Members"]]] + if not (t.InstanceMembers : ApiDocMember list).IsEmpty then tr [] [ td [ColSpan 2. ] [ b [] [!! "Instance Members"]]] yield! t.InstanceMembers |> List.map formatMember - if not t.RecordFields.IsEmpty then tr [] [ td [ColSpan 3. ] [ b [] [!! "Record Fields"]]] - yield! t.RecordFields |> List.map formatMember + // Record Fields from AllMembers + let recordFields = t.AllMembers |> List.filter (fun m -> m.Kind = ApiDocMemberKind.RecordField) + if not recordFields.IsEmpty then tr [] [ td [ColSpan 2. ] [ b [] [!! "Record Fields"]]] + yield! recordFields |> List.map formatMember - if not t.StaticMembers.IsEmpty then tr [] [ td [ColSpan 3. ] [ b [] [!! "Static Members"]]] + if not (t.StaticMembers : ApiDocMember list).IsEmpty then tr [] [ td [ColSpan 2. ] [ b [] [!! "Static Members"]]] yield! t.StaticMembers |> List.map formatMember - if not t.StaticParameters.IsEmpty then tr [] [ td [ColSpan 3. ] [ b [] [!! "Static Parameters"]]] - yield! t.StaticParameters |> List.map formatMember + // Static Parameters from AllMembers + let staticParams = t.AllMembers |> List.filter (fun m -> m.Kind = ApiDocMemberKind.StaticParameter) + if not staticParams.IsEmpty then tr [] [ td [ColSpan 2. ] [ b [] [!! "Static Parameters"]]] + yield! staticParams |> List.map formatMember - if not t.UnionCases.IsEmpty then tr [] [ td [ColSpan 3. ] [ b [] [!! "Union Cases"]]] - yield! t.UnionCases |> List.map formatMember + // Union Cases from AllMembers + let unionCases = t.AllMembers |> List.filter (fun m -> m.Kind = ApiDocMemberKind.UnionCase) + if not unionCases.IsEmpty then tr [] [ td [ColSpan 2. ] [ b [] [!! "Union Cases"]]] + yield! unionCases |> List.map formatMember ] ] - t.UrlName, Layout.layout ctx [body] t.Name + t.UrlBaseName, Layout.layout ctx [body] t.Name -let generateModule ctx (page: ApiPageInfo) = +let generateModule ctx (page: ApiPageInfo) = let m = page.Info let body = div [Class "api-page"] [ - h2 [] [!!m.Name] + h2 [] [!! m.Name] b [] [!! "Namespace: "] - a [Href ($"%s{page.NamespaceUrlName}.html")] [!! page.NamespaceName] - br [] - b [] [!! "Parent: "] - a [Href ($"%s{page.ParentUrlName}.html")] [!! page.ParentName] - span [] [!! (getComment m.Comment)] + a [Href ($"../%s{page.NamespaceUrlName}.html")] [!! page.NamespaceName] br [] + if page.ParentName <> page.NamespaceName then + b [] [!! "Parent Module: "] + a [Href ($"../%s{page.ParentUrlName}.html")] [!! page.ParentName] + br [] if not (String.IsNullOrWhiteSpace m.Category) then - b [] [!! "Category:"] + b [] [!! "Category: "] !!m.Category br [] + br [] + + // Split NestedEntities into types and modules + let nestedTypes = m.NestedEntities |> List.filter (fun e -> e.IsTypeDefinition) + let nestedModules = m.NestedEntities |> List.filter (fun e -> not e.IsTypeDefinition) - if not m.NestedTypes.IsEmpty then + if not nestedTypes.IsEmpty then b [] [!! "Declared Types"] table [] [ tr [] [ th [ Width "35%" ] [!!"Type"] th [ Width "65%"] [!!"Description"] ] - for t in m.NestedTypes do + for t in nestedTypes do tr [] [ - td [] [a [Href ($"%s{t.UrlName}.html")] [!! t.Name ]] + td [] [a [Href ($"%s{t.UrlBaseName}.html")] [!! t.Name ]] td [] [!! (getComment t.Comment)] ] ] br [] - if not m.NestedModules.IsEmpty then + if not nestedModules.IsEmpty then b [] [!! "Declared Modules"] table [] [ tr [] [ th [ Width "35%" ] [!!"Module"] th [ Width "65%"] [!!"Description"] ] - for t in m.NestedModules do + for t in nestedModules do tr [] [ - td [] [a [Href ($"%s{t.UrlName}.html")] [!! t.Name ]] + td [] [a [Href ($"%s{t.UrlBaseName}.html")] [!! t.Name ]] td [] [!! (getComment t.Comment)] ] ] br [] - if not m.ValuesAndFuncs.IsEmpty then + if not (m.ValuesAndFuncs : ApiDocMember list).IsEmpty then b [] [!! "Values and Functions"] table [] [ tr [] [ @@ -179,7 +194,7 @@ let generateModule ctx (page: ApiPageInfo) = ] br [] - if not m.TypeExtensions.IsEmpty then + if not (m.TypeExtensions : ApiDocMember list).IsEmpty then b [] [!! "Type Extensions"] table [] [ tr [] [ @@ -189,45 +204,45 @@ let generateModule ctx (page: ApiPageInfo) = yield! m.TypeExtensions |> List.map formatMember ] ] - m.UrlName, Layout.layout ctx [body] m.Name + m.UrlBaseName, Layout.layout ctx [body] m.Name + +let generateNamespace ctx (allTypes: ApiPageInfo list) (ns: ApiDocNamespace) = + let namespaceTypes = allTypes |> List.filter (fun t -> t.NamespaceName = ns.Name && t.ParentName = ns.Name) -let generateNamespace ctx (n: Namespace) = let body = div [Class "api-page"] [ - h2 [] [!!n.Name] - - if not n.Types.IsEmpty then + h2 [] [!!ns.Name] + if not namespaceTypes.IsEmpty then b [] [!! "Declared Types"] table [] [ tr [] [ th [ Width "35%" ] [!!"Type"] th [ Width "65%"] [!!"Description"] ] - for t in n.Types do + for t in namespaceTypes do tr [] [ - td [] [a [Href ($"%s{t.UrlName}.html")] [!! t.Name ]] - td [] [!!(getComment t.Comment)] + td [] [a [Href ($"%s{t.Info.UrlBaseName}.html")] [!! t.Info.Name ]] + td [] [!! (getComment t.Info.Comment)] ] ] br [] - if not n.Modules.IsEmpty then - + if not (ns.Entities).IsEmpty then b [] [!! "Declared Modules"] table [] [ tr [] [ th [ Width "35%" ] [!!"Module"] th [ Width "65%"] [!!"Description"] ] - for t in n.Modules do + for t in ns.Entities do tr [] [ - td [] [a [Href ($"%s{t.UrlName}.html")] [!! t.Name ]] + td [] [a [Href ($"%s{t.UrlBaseName}.html")] [!! t.Name ]] td [] [!! (getComment t.Comment)] ] ] ] - n.Name, Layout.layout ctx [body] (n.Name) + ns.Name, Layout.layout ctx [body] (ns.Name) let generate' (ctx : SiteContents) = @@ -238,10 +253,10 @@ let generate' (ctx : SiteContents) = all |> Seq.toList |> List.collect (fun n -> - let name = n.GeneratorOutput.AssemblyGroup.Name + let name = n.GeneratorOutput.Collection.CollectionName let namespaces = - n.GeneratorOutput.AssemblyGroup.Namespaces - |> List.map (generateNamespace ctx) + n.GeneratorOutput.Collection.Namespaces + |> List.map (generateNamespace ctx n.Types) let modules = n.Modules diff --git a/docs/generators/lunr.fsx b/docs/generators/lunr.fsx index 35c98b246..590d773e1 100644 --- a/docs/generators/lunr.fsx +++ b/docs/generators/lunr.fsx @@ -1,15 +1,15 @@ -#r "../_lib/Fornax.Core.dll" -#r "../../packages/docs/Newtonsoft.Json/lib/netstandard2.0/Newtonsoft.Json.dll" -#r "../../packages/docs/FSharp.Formatting/lib/netstandard2.0/FSharp.MetadataFormat.dll" +#r "nuget: Fornax.Core, 0.15.1" +#r "nuget: FSharp.Formatting, 20.0.1" + #if !FORNAX #load "../loaders/contentloader.fsx" #load "../loaders/apirefloader.fsx" #load "../loaders/globalloader.fsx" - #endif open Apirefloader -open FSharp.MetadataFormat +open FSharp.Formatting.ApiDocs +open System.Text.Json type Entry = { @@ -18,65 +18,64 @@ type Entry = { content: string } let generate (ctx : SiteContents) (projectRoot: string) (page: string) = - let siteInfo = ctx.TryGetValue().Value - let rootUrl = siteInfo.root_url + let siteInfo = ctx.TryGetValue() + let rootUrl = + match siteInfo with + | Some info -> info.root_url + | None -> "" let pages = ctx.TryGetValues () |> Option.defaultValue Seq.empty let entries = - pages - |> Seq.map (fun n -> - {uri = rootUrl + "/" + n.link.Replace("content/", ""); title = n.title; content = n.text} - ) + pages + |> Seq.map (fun n -> + {uri = rootUrl + "/" + n.link.Replace("content/", ""); title = n.title; content = n.text} + ) let all = ctx.TryGetValues() let refs = - match all with - | None -> [] - | Some all -> - all - |> Seq.toList - |> List.collect (fun n -> - let generatorOutput = n.GeneratorOutput - let allModules = n.Modules - let allTypes = n.Types + match all with + | None -> [] + | Some all -> + all + |> Seq.toList + |> List.collect (fun n -> + let generatorOutput = n.GeneratorOutput + let allModules = n.Modules + let allTypes = n.Types - let gen = - let ctn = - let namespaces = generatorOutput.AssemblyGroup.Namespaces |> Seq.map (fun n -> n.Name) |> String.concat " " - $"%s{generatorOutput.AssemblyGroup.Name} \n %s{namespaces}" - {uri = $"{rootUrl}/reference/%s{n.Label}/index.html"; title = $"%s{n.Label} - API Reference"; content = ctn } + let gen = + let ctn = + let namespaces = generatorOutput.Collection.Namespaces |> Seq.map (fun n -> n.Name) |> String.concat " " + $"%s{generatorOutput.Collection.CollectionName} \n %s{namespaces}" + {uri = $"{rootUrl}/reference/%s{n.Label}/index.html"; title = $"%s{n.Label} - API Reference"; content = ctn } - let mdlsGen = - allModules - |> Seq.map (fun m -> - let m = m.Info - let cnt = - sprintf "%s \n %s \n %s \n %s \n %s \n %s" - m.Name - m.Comment.FullText - (m.NestedModules |> List.map (fun m -> m.Name + " " + m.Comment.FullText ) |> String.concat " ") - (m.NestedTypes |> List.map (fun m -> m.Name + " " + m.Comment.FullText ) |> String.concat " ") - (m.ValuesAndFuncs |> List.map (fun m -> m.Name + " " + m.Comment.FullText ) |> String.concat " ") - (m.TypeExtensions |> List.map (fun m -> m.Name + " " + m.Comment.FullText ) |> String.concat " ") + let mdlsGen = + allModules + |> Seq.map (fun m -> + let m = m.Info + let cnt = + sprintf "%s \n %s \n %s" + m.Name + (m.Comment.Xml |> Option.map string |> Option.defaultValue "") + (m.NestedEntities |> List.map (fun m -> $"{m.Name} {m.Comment.Xml}") |> String.concat " ") + {uri = $"{rootUrl}/reference/%s{n.Label}/%s{m.UrlBaseName}.html"; title = m.Name; content = cnt } + ) - {uri = $"{rootUrl}/reference/%s{n.Label}/%s{m.UrlName}.html"; title = m.Name; content = cnt } - ) + let tsGen = + allTypes + |> Seq.map (fun m -> + let m = m.Info + let cnt = + let getComment xml = xml |> Option.map string |> Option.defaultValue "" + let allMembers = m.AllMembers |> List.map (fun m -> $"{m.Name} {m.Comment.Xml |> getComment}" ) |> String.concat " " + $"%s{m.Name} \n %s{m.Comment.Xml |> getComment} \n %s{allMembers}" - let tsGen = - allTypes - |> Seq.map (fun m -> - let m = m.Info - let cnt = - let allMembers = m.AllMembers |> List.map (fun m -> $"{m.Name} {m.Comment.FullText}" ) |> String.concat " " - $"%s{m.Name} \n %s{m.Comment.FullText} \n %s{allMembers}" - - - {uri = $"{rootUrl}/reference/%s{n.Label}/%s{m.UrlName}.html"; title = m.Name; content = cnt } - ) - [yield! entries; gen; yield! mdlsGen; yield! tsGen] - ) + {uri = $"{rootUrl}/reference/%s{n.Label}/%s{m.UrlBaseName}.html"; title = m.Name; content = cnt } + ) + [yield! entries; gen; yield! mdlsGen; yield! tsGen] + ) [|yield! entries; yield! refs|] - |> Newtonsoft.Json.JsonConvert.SerializeObject + |> JsonSerializer.Serialize diff --git a/docs/generators/page.fsx b/docs/generators/page.fsx index e07291238..34c9d2d81 100644 --- a/docs/generators/page.fsx +++ b/docs/generators/page.fsx @@ -1,4 +1,4 @@ -#r "../_lib/Fornax.Core.dll" +#r "nuget: Fornax.Core, 0.15.1" #load "partials/layout.fsx" open Html diff --git a/docs/generators/partials/footer.fsx b/docs/generators/partials/footer.fsx index 74f47a586..470e41f15 100644 --- a/docs/generators/partials/footer.fsx +++ b/docs/generators/partials/footer.fsx @@ -1,4 +1,4 @@ -#r "../../_lib/Fornax.Core.dll" +#r "nuget: Fornax.Core, 0.15.1" #if !FORNAX #load "../../loaders/contentloader.fsx" #load "../../loaders/pageloader.fsx" @@ -14,8 +14,8 @@ let footer (ctx : SiteContents) = let rootUrl = siteInfo.root_url [ - div [Custom("style", "left: -1000px; overflow: scroll; position: absolute; top: -1000px; border: none; box-sizing: content-box; height: 200px; margin: 0px; padding: 0px; width: 200px;")] [ - div [Custom("style", "border: none; box-sizing: content-box; height: 200px; margin: 0px; padding: 0px; width: 200px;")] [] + div [HtmlProperties.Custom("style", "left: -1000px; overflow: scroll; position: absolute; top: -1000px; border: none; box-sizing: content-box; height: 200px; margin: 0px; padding: 0px; width: 200px;")] [ + div [HtmlProperties.Custom("style", "border: none; box-sizing: content-box; height: 200px; margin: 0px; padding: 0px; width: 200px;")] [] ] script [Src (rootUrl + "/static/js/clipboard.min.js")] [] script [Src (rootUrl + "/static/js/perfect-scrollbar.min.js")] [] diff --git a/docs/generators/partials/header.fsx b/docs/generators/partials/header.fsx index be7ad2e7e..537b31dd1 100644 --- a/docs/generators/partials/header.fsx +++ b/docs/generators/partials/header.fsx @@ -1,4 +1,4 @@ -#r "../../_lib/Fornax.Core.dll" +#r "nuget: Fornax.Core, 0.15.1" #if !FORNAX #load "../../loaders/contentloader.fsx" #load "../../loaders/pageloader.fsx" diff --git a/docs/generators/partials/layout.fsx b/docs/generators/partials/layout.fsx index 62262c067..f6efb3dc5 100644 --- a/docs/generators/partials/layout.fsx +++ b/docs/generators/partials/layout.fsx @@ -1,4 +1,4 @@ -#r "../../_lib/Fornax.Core.dll" +#r "nuget: Fornax.Core, 0.15.1" #if !FORNAX #load "../../loaders/contentloader.fsx" #load "../../loaders/pageloader.fsx" @@ -45,7 +45,7 @@ let layout (ctx : SiteContents) bodyCnt (page: string) = div [ Class "padding highlightable"] [ div [Id "body-inner"] [ span [Id "sidebar-toggle-span"] [ - a [Href "#"; Id "sidebar-toggle"; Custom("data-sidebar-toggle", "") ] [ + a [Href "#"; Id "sidebar-toggle"; HtmlProperties.Custom("data-sidebar-toggle", "") ] [ i [Class "fas fa-bars"] [] !! " navigation" ] diff --git a/docs/generators/partials/menu.fsx b/docs/generators/partials/menu.fsx index 26a056f5c..94fff9d1f 100644 --- a/docs/generators/partials/menu.fsx +++ b/docs/generators/partials/menu.fsx @@ -1,4 +1,4 @@ -#r "../../_lib/Fornax.Core.dll" +#r "nuget: Fornax.Core, 0.15.1" #if !FORNAX #load "../../loaders/apirefloader.fsx" #load "../../loaders/contentloader.fsx" @@ -8,14 +8,16 @@ open Html - let menu (ctx : SiteContents) (page: string) = let shortcuts = ctx.GetValues () - let all = ctx.GetValues() + let all = ctx.TryGetValues() |> Option.defaultValue Seq.empty let content = ctx.GetValues () - let siteInfo = ctx.TryGetValue().Value - let rootUrl = siteInfo.root_url + let siteInfo = ctx.TryGetValue() + let rootUrl = + match siteInfo with + | Some info -> info.root_url + | None -> "" let group = content |> Seq.tryFind (fun n -> n.title = page) |> Option.map (fun n -> n.category) @@ -29,6 +31,25 @@ let menu (ctx : SiteContents) (page: string) = |> Seq.filter (fun n -> n.category = Contentloader.HowTo && not n.hide_menu ) |> Seq.sortBy (fun n -> n.menu_order) + let apiReferencesSection = + if Seq.isEmpty all then + // If no API references are available, don't show the section + [] + else + [ + li [Class "dd-item parent"] [ + a [if group = None then Class "active" else Class ""] [!! "API References"] + ul [Class "child"] [ + for r in all -> + li [Class "dd-item"] [ + a [Href (rootUrl + "/reference/" + r.Label + "/index.html"); if r.Label = page then Class "active" else Class "" ] [ + !! r.Label + ] + ] + ] + ] + ] + let menuHeader = [ li [Class "dd-item"] [ @@ -45,17 +66,7 @@ let menu (ctx : SiteContents) (page: string) = ] ] ] - li [Class "dd-item parent"] [ - a [if group = None then Class "active" else Class ""] [!! "API References"] - ul [Class "child"] [ - for r in all -> - li [Class "dd-item"] [ - a [Href (rootUrl + "/reference/" + r.Label + "/index.html"); if r.Label = page then Class "active" else Class "" ] [ - !! r.Label - ] - ] - ] - ] + yield! apiReferencesSection ] let renderShortcuts = @@ -78,16 +89,20 @@ let menu (ctx : SiteContents) (page: string) = !! """

Built with Fornax""" ] + let title = + match siteInfo with + | Some info -> info.title + | None -> "FSharpLint" nav [Id "sidebar"] [ div [Id "header-wrapper"] [ div [Id "header"] [ - h2 [Id "logo"] [!! siteInfo.title] + h2 [Id "logo"] [!! title] ] div [Class "searchbox"] [ - label [Custom("for", "search-by")] [i [Class "fas fa-search"] []] - input [Custom ("data-search-input", ""); Id "search-by"; Type "search"; Placeholder "Search..."] - span [Custom ("data-search-clear", "")] [i [Class "fas fa-times"] []] + label [HtmlProperties.Custom ("for", "search-by")] [i [Class "fas fa-search"] []] + input [HtmlProperties.Custom ("data-search-input", ""); Id "search-by"; Type "search"; Placeholder "Search..."] + span [HtmlProperties.Custom ("data-search-clear", "")] [i [Class "fas fa-times"] []] ] script [Type "text/javascript"; Src (rootUrl + "/static/js/lunr.min.js")] [] script [Type "text/javascript"; Src (rootUrl + "/static/js/auto-complete.js")] [] diff --git a/docs/loaders/apirefloader.fsx b/docs/loaders/apirefloader.fsx index 0e75204ed..372827c0a 100644 --- a/docs/loaders/apirefloader.fsx +++ b/docs/loaders/apirefloader.fsx @@ -1,9 +1,10 @@ -#r "../_lib/Fornax.Core.dll" -#r "../../packages/docs/FSharp.Formatting/lib/netstandard2.0/FSharp.MetadataFormat.dll" +#r "nuget: Fornax.Core, 0.15.1" +#r "nuget: FSharp.Formatting, 20.0.1" open System open System.IO -open FSharp.MetadataFormat +open FSharp.Formatting.ApiDocs +open FSharp.Formatting.Templating type ApiPageInfo<'a> = { ParentName: string @@ -15,58 +16,102 @@ type ApiPageInfo<'a> = { type AssemblyEntities = { Label: string - Modules: ApiPageInfo list - Types: ApiPageInfo list - GeneratorOutput: GeneratorOutput + Modules: ApiPageInfo list + Types: ApiPageInfo list + GeneratorOutput: ApiDocModel } -let rec collectModules pn pu nn nu (m: Module) = +let rec collectModules pn pu nn nu (m: ApiDocEntity) = [ - yield { ParentName = pn; ParentUrlName = pu; NamespaceName = nn; NamespaceUrlName = nu; Info = m} - yield! m.NestedModules |> List.collect (collectModules m.Name m.UrlName nn nu ) + yield { ParentName = pn; ParentUrlName = pu; NamespaceName = nn; NamespaceUrlName = nu; Info = m} + yield! m.NestedEntities |> List.collect (collectModules m.Name m.UrlBaseName nn nu) ] let loader (projectRoot: string) (siteContet: SiteContents) = try - let dlls = - [ - "FSharpLint.Core", Path.Combine(projectRoot, "..", "build", "FSharpLint.Core.dll") + // We need the console location as it contains all the dependencies + let projectDir = Path.Combine(projectRoot, "..", "src", "FSharpLint.Console") + let dotNetMoniker = "net9.0" + let projectName = "FSharpLint.Console" + let projectArtifactName = "FSharpLint.Core.dll" + // Try multiple possible locations for the assembly + let possiblePaths = [ + // CI build output + Path.Combine("..", "build", projectArtifactName) + // Release build + Path.Combine(projectDir, "bin", "Release", dotNetMoniker, projectArtifactName) + // Debug build + Path.Combine(projectDir, "bin", "Debug", dotNetMoniker, projectArtifactName) + // Default build output (no custom output path) + Path.Combine(projectDir, "bin", "Release", projectArtifactName) + Path.Combine(projectDir, "bin", "Debug", projectArtifactName) ] - let libs = - [ - Path.Combine (projectRoot, "..", "build") - ] - for (label, dll) in dlls do - let output = MetadataFormat.Generate(dll, markDownComments = true, publicOnly = true, libDirs = libs) - let allModules = - output.AssemblyGroup.Namespaces - |> List.collect (fun n -> - List.collect (collectModules n.Name n.Name n.Name n.Name) n.Modules - ) + let foundDll = possiblePaths |> List.tryFind File.Exists + + match foundDll with + | Some dllPath -> + let binDir = Path.GetDirectoryName(dllPath) + printfn $"Found assembly at: %s{dllPath}" + printfn $"Using lib directory: %s{binDir}" + + let libs = [binDir] - let allTypes = - [ - yield! - output.AssemblyGroup.Namespaces + // Try to load with minimal dependencies first + let inputs = [ApiDocInput.FromFile(dllPath, mdcomments = true)] + try + let output = ApiDocs.GenerateModel(inputs, projectName, [], libDirs = libs) + + let allModules = + output.Collection.Namespaces |> List.collect (fun n -> - n.Types |> List.map (fun t -> {ParentName = n.Name; ParentUrlName = n.Name; NamespaceName = n.Name; NamespaceUrlName = n.Name; Info = t} ) + List.collect (collectModules n.Name n.Name n.Name n.Name) n.Entities ) - yield! - allModules - |> List.collect (fun n -> - n.Info.NestedTypes |> List.map (fun t -> {ParentName = n.Info.Name; ParentUrlName = n.Info.UrlName; NamespaceName = n.NamespaceName; NamespaceUrlName = n.NamespaceUrlName; Info = t}) ) - ] - let entities = { - Label = label - Modules = allModules - Types = allTypes - GeneratorOutput = output - } - siteContet.Add entities + + let allTypes = + [ + yield! + output.Collection.Namespaces + |> List.collect (fun n -> + n.Entities |> List.choose (fun t -> + if t.IsTypeDefinition then + Some {ParentName = n.Name; ParentUrlName = n.Name; NamespaceName = n.Name; NamespaceUrlName = n.Name; Info = t} + else + None) + ) + yield! + allModules + |> List.collect (fun n -> + // Get nested types from nested entities + n.Info.NestedEntities + |> List.choose (fun e -> + if e.IsTypeDefinition then + Some {ParentName = n.Info.Name; ParentUrlName = n.Info.UrlBaseName; NamespaceName = n.NamespaceName; NamespaceUrlName = n.NamespaceUrlName; Info = e} + else + None) + ) + ] + let entities = { + Label = "FSharpLint.Core" + Modules = allModules + Types = allTypes + GeneratorOutput = output + } + siteContet.Add entities + printfn $"Successfully loaded API documentation for {projectName}" + with + | ex -> + printfn $"Failed to generate API docs from %s{dllPath}: %A{ex}" + printfn "Continuing without API documentation..." + | None -> + printfn $"Warning: Could not find {projectArtifactName} in any of the expected locations:" + possiblePaths |> List.iter (printfn " - %s") + printfn "API documentation will not be generated." + Environment.Exit 1 with | ex -> - printfn "%A" ex + printfn "Error in API reference loader: %A" ex + Environment.Exit 1 siteContet diff --git a/docs/loaders/contentloader.fsx b/docs/loaders/contentloader.fsx index c101a5ca3..3cb4574ff 100644 --- a/docs/loaders/contentloader.fsx +++ b/docs/loaders/contentloader.fsx @@ -1,8 +1,7 @@ -open System -#r "../_lib/Fornax.Core.dll" -#r "../../packages/docs/Markdig/lib/netstandard2.0/Markdig.dll" - +#r "nuget: Fornax.Core, 0.15.1" +#r "nuget: Markdig, 0.41.3" open Markdig +open System open System.IO type PostConfig = { diff --git a/docs/loaders/copyloader.fsx b/docs/loaders/copyloader.fsx index e2b892f08..75ce70edb 100644 --- a/docs/loaders/copyloader.fsx +++ b/docs/loaders/copyloader.fsx @@ -1,4 +1,4 @@ -#r "../_lib/Fornax.Core.dll" +#r "nuget: Fornax.Core, 0.15.1" open System.IO @@ -15,4 +15,4 @@ let loader (projectRoot: string) (siteContet: SiteContents) = for filePath in Directory.GetFiles(intputPath, "*.*", SearchOption.AllDirectories) do File.Copy(filePath, filePath.Replace(intputPath, outputPath), true) - siteContet \ No newline at end of file + siteContet diff --git a/docs/loaders/globalloader.fsx b/docs/loaders/globalloader.fsx index 619eefbd9..e128e6f43 100644 --- a/docs/loaders/globalloader.fsx +++ b/docs/loaders/globalloader.fsx @@ -1,4 +1,4 @@ -#r "../_lib/Fornax.Core.dll" +#r "nuget: Fornax.Core, 0.15.1" type SiteInfo = { title: string diff --git a/docs/loaders/pageloader.fsx b/docs/loaders/pageloader.fsx index 8a0a4dda0..41cea74ba 100644 --- a/docs/loaders/pageloader.fsx +++ b/docs/loaders/pageloader.fsx @@ -1,5 +1,4 @@ -#r "../_lib/Fornax.Core.dll" - +#r "nuget: Fornax.Core, 0.15.1" type Shortcut = { title: string diff --git a/global.json b/global.json index 5461033ae..a4155b168 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "rollForward": "minor" + "version": "9.0.201", + "rollForward": "disable" } } diff --git a/src/FSharpLint.Core/AssemblyInfo.fs b/src/FSharpLint.Core/Attributes.fs similarity index 100% rename from src/FSharpLint.Core/AssemblyInfo.fs rename to src/FSharpLint.Core/Attributes.fs diff --git a/src/FSharpLint.Core/FSharpLint.Core.fsproj b/src/FSharpLint.Core/FSharpLint.Core.fsproj index c73bdac00..c594ae2e7 100644 --- a/src/FSharpLint.Core/FSharpLint.Core.fsproj +++ b/src/FSharpLint.Core/FSharpLint.Core.fsproj @@ -7,6 +7,7 @@ true FSharpLint.Core false + false FSharpLint.Core API to programmatically run FSharpLint. @@ -15,7 +16,7 @@ - +