mirror of
https://github.com/Smaug123/website-lint
synced 2025-10-12 02:08:42 +00:00
Bare-bones integrity checker
This commit is contained in:
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"fantomas": {
|
||||
"version": "5.2.0-alpha-010",
|
||||
"commands": [
|
||||
"fantomas"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
41
.editorconfig
Normal file
41
.editorconfig
Normal file
@@ -0,0 +1,41 @@
|
||||
root=true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line=crlf
|
||||
trim_trailing_whitespace=true
|
||||
insert_final_newline=true
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
# ReSharper properties
|
||||
resharper_xml_indent_size=2
|
||||
resharper_xml_max_line_length=100
|
||||
resharper_xml_tab_width=2
|
||||
|
||||
[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
|
||||
[*.{fs,fsi}]
|
||||
fsharp_bar_before_discriminated_union_declaration=true
|
||||
fsharp_space_before_uppercase_invocation=true
|
||||
fsharp_space_before_class_constructor=true
|
||||
fsharp_space_before_member=true
|
||||
fsharp_space_before_colon=true
|
||||
fsharp_space_before_semicolon=true
|
||||
fsharp_multiline_block_brackets_on_same_column=true
|
||||
fsharp_newline_between_type_definition_and_members=true
|
||||
fsharp_align_function_signature_to_indentation=true
|
||||
fsharp_alternative_long_member_definitions=true
|
||||
fsharp_multi_line_lambda_closing_newline=true
|
||||
fsharp_experimental_keep_indent_in_branch=true
|
||||
fsharp_max_value_binding_width=80
|
||||
fsharp_max_record_width=0
|
||||
max_line_length=120
|
||||
end_of_line=lf
|
||||
|
||||
[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
tab_width=2
|
95
.github/workflows/dotnetcore.yml
vendored
Normal file
95
.github/workflows/dotnetcore.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
name: .NET
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
DOTNET_NOLOGO: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- Release
|
||||
- Debug
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --no-restore --configuration ${{matrix.config}}
|
||||
- name: Test
|
||||
run: dotnet test --no-build --verbosity normal --configuration ${{matrix.config}}
|
||||
|
||||
check-dotnet-format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v17
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run Fantomas
|
||||
run: nix run .#fantomas -- -r --check .
|
||||
|
||||
check-nix-format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v17
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run Alejandra
|
||||
run: nix develop --command alejandra --check .
|
||||
|
||||
shellcheck:
|
||||
name: Shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v17
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run ShellCheck
|
||||
run: nix develop --command bash -c "find . -type f -name '*.sh' | xargs shellcheck"
|
||||
|
||||
nix-build:
|
||||
name: Nix build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v17
|
||||
with:
|
||||
extra_nix_config: |
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build app
|
||||
run: nix build
|
||||
|
||||
all-required-checks-complete:
|
||||
needs: [check-dotnet-format, check-nix-format, build, nix-build, shellcheck]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "All required checks complete."
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea/
|
||||
result
|
16
Integrity.sln
Normal file
16
Integrity.sln
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Integrity", "Integrity\Integrity.fsproj", "{8CD2D930-0A0D-493D-80A9-C59259847265}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{8CD2D930-0A0D-493D-80A9-C59259847265}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8CD2D930-0A0D-493D-80A9-C59259847265}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8CD2D930-0A0D-493D-80A9-C59259847265}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8CD2D930-0A0D-493D-80A9-C59259847265}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
20
Integrity/Integrity.fsproj
Normal file
20
Integrity/Integrity.fsproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Result.fs" />
|
||||
<Compile Include="Link.fs" />
|
||||
<Compile Include="Program.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Argu" Version="6.1.1" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="17.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
100
Integrity/Link.fs
Normal file
100
Integrity/Link.fs
Normal file
@@ -0,0 +1,100 @@
|
||||
namespace Integrity
|
||||
|
||||
open System
|
||||
open System.Collections.Concurrent
|
||||
open System.Collections.Generic
|
||||
open System.IO.Abstractions
|
||||
open System.Threading
|
||||
open System.Threading.Tasks
|
||||
open HtmlAgilityPack
|
||||
|
||||
type LinkError = | NoHref of IFileInfo * HtmlNode
|
||||
|
||||
type Link = | Link of string
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module Link =
|
||||
|
||||
let lintFile (file : IFileInfo) : Link IReadOnlyList * LinkError IReadOnlyList =
|
||||
let fs = file.FileSystem
|
||||
let doc = HtmlDocument ()
|
||||
fs.File.ReadAllText file.FullName |> doc.LoadHtml
|
||||
|
||||
match doc.DocumentNode with
|
||||
| null -> failwith $"Unexpectedly got a null DocumentNode on {file.FullName}"
|
||||
| doc ->
|
||||
|
||||
if isNull (doc.SelectNodes "//body") then
|
||||
[], []
|
||||
|
||||
else
|
||||
|
||||
match doc.SelectNodes "//a[@href]" with
|
||||
| null ->
|
||||
// why would you give `null` when what you really mean is an empty collection :(
|
||||
[], []
|
||||
| nodes ->
|
||||
|
||||
nodes
|
||||
|> Seq.choose (fun node ->
|
||||
match node.Attributes with
|
||||
| null -> failwith "Unexpectedly got no attributes on node"
|
||||
|
||||
| attrs ->
|
||||
|
||||
match attrs.["href"] with
|
||||
| null -> Error (NoHref (file, node)) |> Some
|
||||
| v -> Ok (Link v.Value) |> Some
|
||||
)
|
||||
|> Result.partition
|
||||
|
||||
type private Fetcher<'a> =
|
||||
| Claimed of int
|
||||
| WaitFor of Task<'a>
|
||||
|
||||
let validateExternal
|
||||
(sleep : TimeSpan -> unit Async)
|
||||
(fetchHtml : Uri -> Result<string, string> Async)
|
||||
: Uri -> Async<Result<string, string>>
|
||||
=
|
||||
let store = ConcurrentDictionary ()
|
||||
let counter = ref 0
|
||||
|
||||
let forceSuccess =
|
||||
// These websites are flaky or slow, but we assume I've got them right.
|
||||
[
|
||||
"web.archive.org", "slow to access"
|
||||
"news.ycombinator.com", "flaky and forbids access"
|
||||
"www.linkedin.com", "forbids access"
|
||||
]
|
||||
|> Map.ofList
|
||||
|
||||
let rec get (uri : Uri) =
|
||||
match Map.tryFind uri.Host forceSuccess with
|
||||
| Some reason -> async.Return (Ok $"%s{uri.Host}: %s{reason}")
|
||||
| None ->
|
||||
|
||||
async {
|
||||
let me = Interlocked.Increment counter
|
||||
|
||||
let added =
|
||||
store.GetOrAdd (uri, Func<_, _, _> (fun uri () -> Fetcher.Claimed me), ())
|
||||
|
||||
match added with
|
||||
| Fetcher.Claimed i when i = me ->
|
||||
let running = fetchHtml uri |> Async.StartAsTask
|
||||
store.[uri] <- WaitFor running
|
||||
let! result = running |> Async.AwaitTask
|
||||
|
||||
match result with
|
||||
| Error e -> Console.WriteLine $"Error fetching {uri}: {e}"
|
||||
| Ok _ -> ()
|
||||
|
||||
return result
|
||||
| Fetcher.Claimed _ ->
|
||||
do! sleep (TimeSpan.FromMilliseconds 50.0)
|
||||
return! get uri
|
||||
| Fetcher.WaitFor t -> return! t |> Async.AwaitTask
|
||||
}
|
||||
|
||||
get
|
225
Integrity/Program.fs
Normal file
225
Integrity/Program.fs
Normal file
@@ -0,0 +1,225 @@
|
||||
namespace Integrity
|
||||
|
||||
open System
|
||||
open System.Collections.Generic
|
||||
open System.IO
|
||||
open System.IO.Abstractions
|
||||
open System.Net.Http
|
||||
open System.Net.Http.Headers
|
||||
open Argu
|
||||
|
||||
type LinkValidationError =
|
||||
| DidNotExistOnDisk
|
||||
| DidNotExistOnInternet of errorString : string
|
||||
|
||||
override this.ToString () =
|
||||
match this with
|
||||
| LinkValidationError.DidNotExistOnDisk -> "(internal link)"
|
||||
| LinkValidationError.DidNotExistOnInternet e -> sprintf "(via Internet, error: %s)" e
|
||||
|
||||
type RelativeHtmlPath =
|
||||
| RelativeHtmlPath of string
|
||||
|
||||
override this.ToString () =
|
||||
match this with
|
||||
| RelativeHtmlPath p -> p
|
||||
|
||||
type ArgsFragment =
|
||||
| [<ExactlyOnce ; EqualsAssignmentOrSpaced>] Website of string
|
||||
| [<ExactlyOnce ; EqualsAssignmentOrSpaced>] External_Link_File of string
|
||||
| [<ExactlyOnce ; EqualsAssignmentOrSpaced>] Root_Folder of string
|
||||
|
||||
interface IArgParserTemplate with
|
||||
member s.Usage =
|
||||
match s with
|
||||
| Website _ -> "the website you're scanning - we won't hit this, it's used to filter out local links"
|
||||
| External_Link_File _ ->
|
||||
"path to a text file containing a newline-delimited list of links to pages of the website that must be there"
|
||||
| Root_Folder _ -> "folder we'll scan, intended to be served as a static site"
|
||||
|
||||
type Args =
|
||||
{
|
||||
Website : string
|
||||
ExternalLinkFile : IFileInfo
|
||||
RootFolder : IDirectoryInfo
|
||||
}
|
||||
|
||||
module Program =
|
||||
|
||||
let fetchHtml (httpClient : HttpClient) (u : Uri) : Result<string, string> Async =
|
||||
async {
|
||||
let! result = httpClient.GetAsync u |> Async.AwaitTask |> Async.Catch
|
||||
|
||||
match result with
|
||||
| Choice1Of2 result ->
|
||||
if result.IsSuccessStatusCode then
|
||||
let! content = result.Content.ReadAsStringAsync () |> Async.AwaitTask
|
||||
return Ok content
|
||||
else
|
||||
return Error result.ReasonPhrase
|
||||
| Choice2Of2 result -> return Error result.Message
|
||||
}
|
||||
|
||||
/// Returns any broken links.
|
||||
let validateLinks<'a>
|
||||
(sleep : TimeSpan -> unit Async)
|
||||
(getLink : 'a -> Link IReadOnlyList)
|
||||
(fetchHtml : Uri -> Result<string, string> Async)
|
||||
(website : string)
|
||||
(rootFolder : IDirectoryInfo)
|
||||
(allLinks : Map<RelativeHtmlPath, 'a>)
|
||||
: (RelativeHtmlPath * (Link * LinkValidationError) list) list
|
||||
=
|
||||
|
||||
let validateExternalLink = Link.validateExternal sleep fetchHtml
|
||||
|
||||
let fs = rootFolder.FileSystem
|
||||
|
||||
allLinks
|
||||
|> Map.toSeq
|
||||
|> Seq.choose (fun (path, links) ->
|
||||
let badLinks =
|
||||
getLink links
|
||||
|> Seq.map (fun (Link linkPath as link) ->
|
||||
let linkPath =
|
||||
if linkPath.StartsWith website then
|
||||
linkPath.Substring website.Length
|
||||
else
|
||||
linkPath
|
||||
|
||||
if linkPath.StartsWith '/' then
|
||||
let candidates =
|
||||
[
|
||||
yield linkPath
|
||||
yield
|
||||
if linkPath.EndsWith '/' then
|
||||
$"{linkPath}index.html"
|
||||
else
|
||||
$"{linkPath}/index.html"
|
||||
]
|
||||
|
||||
if
|
||||
candidates
|
||||
|> List.exists (fun c -> Map.tryFind (RelativeHtmlPath c) allLinks |> Option.isSome)
|
||||
then
|
||||
// Successfully looked up this page; ignore, as it's not an error.
|
||||
async.Return None
|
||||
else
|
||||
// Not an HTML file, or not found; look it up on the disk.
|
||||
let f =
|
||||
fs.Path.Combine (rootFolder.FullName, linkPath.TrimStart '/')
|
||||
|> fs.FileInfo.FromFileName
|
||||
|
||||
if f.Exists then
|
||||
async.Return None
|
||||
else
|
||||
Some (link, DidNotExistOnDisk) |> async.Return
|
||||
elif linkPath.[0] = '#' then
|
||||
// An anchor link
|
||||
// TODO: make sure the anchor exists
|
||||
None |> async.Return
|
||||
else
|
||||
// An external link!
|
||||
let uri =
|
||||
try
|
||||
Uri linkPath
|
||||
with e ->
|
||||
eprintfn "%s" linkPath
|
||||
reraise ()
|
||||
|
||||
let unsupported = Set.ofList [ "mailto" ]
|
||||
|
||||
if unsupported |> Set.contains uri.Scheme then
|
||||
async.Return None
|
||||
else
|
||||
|
||||
async {
|
||||
match! validateExternalLink uri with
|
||||
| Ok _ -> return None
|
||||
| Error e -> return Some (link, DidNotExistOnInternet e)
|
||||
}
|
||||
)
|
||||
|> Async.Parallel
|
||||
|> Async.RunSynchronously
|
||||
|> Seq.choose id
|
||||
|> Seq.toList
|
||||
|
||||
match badLinks with
|
||||
| [] -> None
|
||||
| badLinks -> Some (path, badLinks)
|
||||
)
|
||||
|> Seq.toList
|
||||
|
||||
[<EntryPoint>]
|
||||
let main argv =
|
||||
let fs = FileSystem ()
|
||||
|
||||
let parser = ArgumentParser.Create<ArgsFragment> ()
|
||||
let parsed = parser.Parse argv
|
||||
|
||||
let args =
|
||||
{
|
||||
Website = parsed.GetResult ArgsFragment.Website
|
||||
ExternalLinkFile = parsed.GetResult ArgsFragment.External_Link_File |> fs.FileInfo.FromFileName
|
||||
RootFolder = parsed.GetResult ArgsFragment.Root_Folder |> fs.DirectoryInfo.FromDirectoryName
|
||||
}
|
||||
|
||||
let allLinks =
|
||||
args.RootFolder.EnumerateFiles ("*.html", SearchOption.AllDirectories)
|
||||
|> Seq.map (fun file ->
|
||||
let name = file.FullName.Substring (args.RootFolder.FullName.Length - 1)
|
||||
RelativeHtmlPath name, Link.lintFile file
|
||||
)
|
||||
|> Map.ofSeq
|
||||
|
||||
let whoHasLinkedToUs = args.ExternalLinkFile.FullName |> fs.File.ReadAllLines
|
||||
|
||||
let weHaveBrokenThirdParties =
|
||||
whoHasLinkedToUs
|
||||
|> Seq.choose (fun link ->
|
||||
if not <| link.StartsWith args.Website then
|
||||
failwith "they linked to a different website?"
|
||||
|
||||
let link = link.Substring (args.Website.Length + 1)
|
||||
let link = if link.EndsWith '/' then $"{link}/index.html" else link
|
||||
|
||||
let f = fs.Path.Combine (args.RootFolder.FullName, link) |> fs.FileInfo.FromFileName
|
||||
if f.Exists then None else Some (RelativeHtmlPath link)
|
||||
)
|
||||
|> Seq.toList
|
||||
|
||||
for broken in weHaveBrokenThirdParties do
|
||||
eprintfn $"We have broken a third-party link to {broken}"
|
||||
|
||||
use httpClient = new HttpClient ()
|
||||
|
||||
[
|
||||
"Mozilla/5.0"
|
||||
"(Macintosh; Intel Mac OS X 10_15_7)"
|
||||
"AppleWebKit/537.36"
|
||||
"(KHTML, like Gecko)"
|
||||
"Chrome/104.0.0.0"
|
||||
"Safari/537.36"
|
||||
]
|
||||
|> List.iter (ProductInfoHeaderValue.Parse >> httpClient.DefaultRequestHeaders.UserAgent.Add)
|
||||
|
||||
// Check internal links
|
||||
let nonExistentInternalLinks =
|
||||
validateLinks Async.Sleep fst (fetchHtml httpClient) args.Website args.RootFolder allLinks
|
||||
|> Seq.collect (fun (localLink, errors) ->
|
||||
errors
|
||||
|> List.map (fun (link, error) -> link, error, localLink)
|
||||
|> List.groupBy (fun (link, _, _) -> link)
|
||||
|> Seq.map (fun (link, errors) -> link, errors |> List.map (fun (_, error, path) -> error, path))
|
||||
)
|
||||
|> Map.ofSeq
|
||||
|
||||
for KeyValue (Link url, links) in nonExistentInternalLinks do
|
||||
links
|
||||
|> Seq.map (fun (error, RelativeHtmlPath localPath) ->
|
||||
sprintf "%s %s" localPath (string<LinkValidationError> error)
|
||||
)
|
||||
|> String.concat "\n "
|
||||
|> eprintfn "The following links to %s did not resolve:\n %s" url
|
||||
|
||||
0
|
17
Integrity/Result.fs
Normal file
17
Integrity/Result.fs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Integrity
|
||||
|
||||
open System.Collections.Generic
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module Result =
|
||||
|
||||
let partition<'a, 'b> (l : Result<'a, 'b> seq) : 'a IReadOnlyList * 'b IReadOnlyList =
|
||||
let oks = ResizeArray<'a> ()
|
||||
let errors = ResizeArray<'b> ()
|
||||
|
||||
for result in l do
|
||||
match result with
|
||||
| Ok a -> oks.Add a
|
||||
| Error b -> errors.Add b
|
||||
|
||||
oks, errors
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Patrick Stevens
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
42
flake.lock
generated
Normal file
42
flake.lock
generated
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1672350804,
|
||||
"narHash": "sha256-jo6zkiCabUBn3ObuKXHGqqORUMH27gYDIFFfLq5P4wg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "677ed08a50931e38382dbef01cba08a8f7eac8f6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
89
flake.nix
Normal file
89
flake.nix
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
description = "Link integrity checker for my website";
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system: let
|
||||
pkgs = import nixpkgs {inherit system;};
|
||||
projectFile = "./Integrity/Integrity.fsproj";
|
||||
testProjectFiles = [];
|
||||
pname = "Integrity";
|
||||
dotnet-sdk = pkgs.dotnet-sdk_7;
|
||||
dotnet-runtime = pkgs.dotnetCorePackages.runtime_7_0;
|
||||
version = "0.0.1";
|
||||
dotnetTool = toolName: toolVersion: sha256:
|
||||
pkgs.stdenvNoCC.mkDerivation rec {
|
||||
name = toolName;
|
||||
version = toolVersion;
|
||||
nativeBuildInputs = [pkgs.makeWrapper];
|
||||
src = pkgs.fetchNuGet {
|
||||
pname = name;
|
||||
version = version;
|
||||
sha256 = sha256;
|
||||
installPhase = ''mkdir -p $out/bin && cp -r tools/net6.0/any/* $out/bin'';
|
||||
};
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p "$out/lib"
|
||||
cp -r ./bin/* "$out/lib"
|
||||
makeWrapper "${dotnet-runtime}/bin/dotnet" "$out/bin/${name}" --add-flags "$out/lib/${name}.dll"
|
||||
runHook postInstall
|
||||
'';
|
||||
};
|
||||
in {
|
||||
packages = {
|
||||
fantomas = dotnetTool "fantomas" "5.2.0-alpha-010" "sha256-CuoROZBBhaK0IFjbKNLvzgX4GXwuIybqIvCtuqROBMk=";
|
||||
fetchDeps = let
|
||||
flags = [];
|
||||
runtimeIds = map (system: pkgs.dotnetCorePackages.systemToDotnetRid system) dotnet-sdk.meta.platforms;
|
||||
in
|
||||
pkgs.writeShellScript "fetch-${pname}-deps" (builtins.readFile (pkgs.substituteAll {
|
||||
src = ./nix/fetchDeps.sh;
|
||||
pname = pname;
|
||||
binPath = pkgs.lib.makeBinPath [pkgs.coreutils dotnet-sdk (pkgs.nuget-to-nix.override {inherit dotnet-sdk;})];
|
||||
projectFiles = toString (pkgs.lib.toList projectFile);
|
||||
testProjectFiles =
|
||||
if testProjectFiles == []
|
||||
then ""
|
||||
else toString testProjectFiles;
|
||||
rids = pkgs.lib.concatStringsSep "\" \"" runtimeIds;
|
||||
packages = dotnet-sdk.packages;
|
||||
storeSrc = pkgs.srcOnly {
|
||||
src = ./.;
|
||||
pname = pname;
|
||||
version = version;
|
||||
};
|
||||
}));
|
||||
default = pkgs.buildDotnetModule {
|
||||
pname = pname;
|
||||
version = version;
|
||||
src = ./.;
|
||||
projectFile = projectFile;
|
||||
nugetDeps = ./nix/deps.nix;
|
||||
doCheck = true;
|
||||
dotnet-sdk = dotnet-sdk;
|
||||
dotnet-runtime = dotnet-runtime;
|
||||
};
|
||||
};
|
||||
devShells = let
|
||||
requirements = [];
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
pkgs.dotnet-sdk_7
|
||||
pkgs.git
|
||||
pkgs.alejandra
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
34
nix/deps.nix
Normal file
34
nix/deps.nix
Normal file
@@ -0,0 +1,34 @@
|
||||
# This file was automatically generated by passthru.fetch-deps.
|
||||
# Please don't edit it manually, your changes might get overwritten!
|
||||
{fetchNuGet}: [
|
||||
(fetchNuGet {
|
||||
pname = "Argu";
|
||||
version = "6.1.1";
|
||||
sha256 = "1v996g0760qhiys2ahdpnvkldaxr2jn5f1falf789glnk4a6f3xl";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "FSharp.Core";
|
||||
version = "7.0.0";
|
||||
sha256 = "1pgk3qk9p1s53wvja17744x4bf7zs3a3wf0dmxi66w1w06z7i85x";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "HtmlAgilityPack";
|
||||
version = "1.11.43";
|
||||
sha256 = "08xh6fm5l9f8lhhkk0h9vrp8qa60qmiq8k6wyip8lqn810jld50m";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "System.Configuration.ConfigurationManager";
|
||||
version = "4.4.0";
|
||||
sha256 = "1hjgmz47v5229cbzd2pwz2h0dkq78lb2wp9grx8qr72pb5i0dk7v";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "System.IO.Abstractions";
|
||||
version = "17.1.1";
|
||||
sha256 = "1pkqigqa0f62ma7cxhz5qiwfb7h6dmiszvzl3gr3pczc7ff4v3fi";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "System.Security.Cryptography.ProtectedData";
|
||||
version = "4.4.0";
|
||||
sha256 = "1q8ljvqhasyynp94a1d7jknk946m20lkwy2c3wa8zw2pc517fbj6";
|
||||
})
|
||||
]
|
73
nix/fetchDeps.sh
Normal file
73
nix/fetchDeps.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This file was adapted from
|
||||
# https://github.com/NixOS/nixpkgs/blob/b981d811453ab84fb3ea593a9b33b960f1ab9147/pkgs/build-support/dotnet/build-dotnet-module/default.nix#L173
|
||||
set -euo pipefail
|
||||
export PATH="@binPath@"
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--keep-sources|-k)
|
||||
keepSources=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
echo "usage: $0 [--keep-sources] [--help] <output path>"
|
||||
echo " <output path> The path to write the lockfile to. A temporary file is used if this is not set"
|
||||
echo " --keep-sources Don't remove temporary directories upon exit, useful for debugging"
|
||||
echo " --help Show this help message"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
done
|
||||
tmp=$(mktemp -td "@pname@-tmp-XXXXXX")
|
||||
export tmp
|
||||
HOME=$tmp/home
|
||||
exitTrap() {
|
||||
test -n "${ranTrap-}" && return
|
||||
ranTrap=1
|
||||
if test -n "${keepSources-}"; then
|
||||
echo -e "Path to the source: $tmp/src\nPath to the fake home: $tmp/home"
|
||||
else
|
||||
rm -rf "$tmp"
|
||||
fi
|
||||
# Since mktemp is used this will be empty if the script didnt succesfully complete
|
||||
if ! test -s "$depsFile"; then
|
||||
rm -rf "$depsFile"
|
||||
fi
|
||||
}
|
||||
trap exitTrap EXIT INT TERM
|
||||
dotnetRestore() {
|
||||
local -r project="${1-}"
|
||||
local -r rid="$2"
|
||||
dotnet restore "${project-}" \
|
||||
-p:ContinuousIntegrationBuild=true \
|
||||
-p:Deterministic=true \
|
||||
--packages "$tmp/nuget_pkgs" \
|
||||
--runtime "$rid" \
|
||||
--no-cache \
|
||||
--force
|
||||
}
|
||||
declare -a projectFiles=( @projectFiles@ )
|
||||
declare -a testProjectFiles=( @testProjectFiles@ )
|
||||
export DOTNET_NOLOGO=1
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
depsFile=$(realpath "${1:-$(mktemp -t "@pname@-deps-XXXXXX.nix")}")
|
||||
mkdir -p "$tmp/nuget_pkgs"
|
||||
storeSrc="@storeSrc@"
|
||||
src="$tmp/src"
|
||||
cp -rT "$storeSrc" "$src"
|
||||
chmod -R +w "$src"
|
||||
cd "$src"
|
||||
echo "Restoring project..."
|
||||
rids=("@rids@")
|
||||
for rid in "${rids[@]}"; do
|
||||
(( ${#projectFiles[@]} == 0 )) && dotnetRestore "" "$rid"
|
||||
for project in "${projectFiles[@]-}" "${testProjectFiles[@]-}"; do
|
||||
dotnetRestore "$project" "$rid"
|
||||
done
|
||||
done
|
||||
echo "Successfully restored project"
|
||||
echo "Writing lockfile..."
|
||||
echo -e "# This file was automatically generated by passthru.fetch-deps.\n# Please don't edit it manually, your changes might get overwritten!\n" > "$depsFile"
|
||||
nuget-to-nix "$tmp/nuget_pkgs" "@packages@" >> "$depsFile"
|
||||
echo "Successfully wrote lockfile to $depsFile"
|
Reference in New Issue
Block a user