Bare-bones integrity checker

This commit is contained in:
Smaug123
2023-01-01 00:30:34 +00:00
commit eaa9c2ed31
14 changed files with 792 additions and 0 deletions

12
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fantomas": {
"version": "5.2.0-alpha-010",
"commands": [
"fantomas"
]
}
}
}

41
.editorconfig Normal file
View 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
View 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
View File

@@ -0,0 +1,7 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea/
result

16
Integrity.sln Normal file
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"