This commit is contained in:
Janik Haag 2024-07-26 00:52:31 +02:00
commit 5bf7d0ef20
No known key found for this signature in database
15 changed files with 452 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.direnv
result
packages.json
options.json

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Dracula Theme
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.

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# search.forkos.org
todo

13
default.nix Normal file
View file

@ -0,0 +1,13 @@
{
pins ? import ./npins,
pkgs ? import pins.nixpkgs { },
nixpkgsSource ? pins.nixpkgs,
}: let
lib = pkgs.lib;
in {
frontend = pkgs.callPackage ./frontend {};
updateIndex = pkgs.callPackage ./updateIndex { };
packagesJSON = pkgs.callPackage ./packagesJSON.nix { nixpkgs = nixpkgsSource; };
optionsJSON = pkgs.callPackage ./optionsJSON.nix { nixpkgs = nixpkgsSource; };
vmTest = "foo";
}

10
frontend/default.nix Normal file
View file

@ -0,0 +1,10 @@
{ lib
, stdenv
}: stdenv.mkDerivation {
pname = "frontend";
version = "0.1.0";
src = ./.;
# todo
}

106
frontend/index.html Normal file
View file

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@meilisearch/instant-meilisearch/templates/basic_search.css" />
</head>
<body>
<div class="options">
<select onchange="kind()" size=2 id="kind">
<option value="packages">packages</option>
<option value="modules">modules</option>
</select>
<select onchange="release()" size=2 id="release">
<option value="rolling">rolling</option>
<option value="stable">stable</option>
</select>
<div id="sort-by"></div>
<div id="packages_filter" class="filter">
<h2>Platform</h2>
<div id="platform"></div>
</div>
<div id="modules_filter" class="filter">
</div>
</div>
<div class="wrapper">
<div id="searchbox" focus></div>
<div id="hits"></div>
</div>
</body>
<script
src="https://cdn.jsdelivr.net/npm/@meilisearch/instant-meilisearch/dist/instant-meilisearch.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4"></script>
<script>
function kind() {
k = document.getElementById("kind").value;
alert(k);
}
function release() {
r = document.getElementById("release").value;
search.helper.setIndex("packages" + "-" + r).search()
}
const search = instantsearch({
indexName: "packages-rolling",
// implement params for stable/rolling
// https://marcus-obst.de/blog/algolia-instantsearch-router-handle-existing-url-parameters
routing: true,
searchClient: instantMeiliSearch(
"https://meilisearch.aq0.de/"
).searchClient
});
search.addWidgets([
// // todo: figure out sortableIndex first
// // https://www.meilisearch.com/docs/learn/filtering_and_sorting/sort_search_results#configure-meilisearch-for-sorting-at-search-time
// // https://www.algolia.com/doc/api-reference/widgets/sort-by/js/
// instantsearch.widgets.sortBy({
// container: '#sort-by',
// items: [
// {value: 'steam-video-games', label: 'Relevant'},
// {
// value: 'steam-video-games:recommendationCount:desc',
// label: 'Most Recommended',
// },
// {
// value: 'steam-video-games:recommendationCount:asc',
// label: 'Least Recommended',
// },
// ],
// }),
instantsearch.widgets.refinementList({
container: "#platform",
attribute: "meta.platforms",
}),
instantsearch.widgets.sortBy({
container: '#sort-by',
items: [
{value: 'steam-video-games', label: 'Relevant'},
{
value: 'steam-video-games:recommendationCount:desc',
label: 'Most Recommended',
},
{
value: 'steam-video-games:recommendationCount:asc',
label: 'Least Recommended',
},
],
}),
instantsearch.widgets.searchBox({container: "#searchbox"}),
instantsearch.widgets.configure({hitsPerPage: 8}),
instantsearch.widgets.hits({
container: "#hits",
templates: {
item: `
<div>
<div class="hit-name">
{{#helpers.highlight}}{"attribute": "name"}{{/helpers.highlight}}
</div>
</div>
`}
})]);
search.start();
</script>
</html>

85
frontend/options.html Normal file
View file

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@meilisearch/instant-meilisearch/templates/basic_search.css" />
</head>
<body>
<div class="options">
<select onchange="kind()" size=2 id="kind">
<option value="packages">packages</option>
<option value="modules">modules</option>
</select>
<select onchange="release()" size=2 id="release">
<option value="rolling">rolling</option>
<option value="stable">stable</option>
</select>
<div id="sort-by"></div>
<!-- <div id="packages_filter" class="filter"> -->
<!-- <h2>Platform</h2> -->
<!-- <div id="platform"></div> -->
<!-- </div> -->
<div id="modules_filter" class="filter">
</div>
</div>
<div class="wrapper">
<div id="searchbox" focus></div>
<div id="hits"></div>
</div>
</body>
<script
src="https://cdn.jsdelivr.net/npm/@meilisearch/instant-meilisearch/dist/instant-meilisearch.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4"></script>
<script>
function kind() {
k = document.getElementById("kind").value;
alert(k);
}
function release() {
r = document.getElementById("release").value;
search.helper.setIndex("options" + "-" + r).search()
}
const search = instantsearch({
indexName: "options-rolling",
// implement params for stable/rolling
// https://marcus-obst.de/blog/algolia-instantsearch-router-handle-existing-url-parameters
routing: true,
searchClient: instantMeiliSearch(
"https://meilisearch.aq0.de/"
).searchClient
});
search.addWidgets([
instantsearch.widgets.sortBy({
container: '#sort-by',
items: [
{value: 'steam-video-games', label: 'Relevant'},
{
value: 'steam-video-games:recommendationCount:desc',
label: 'Most Recommended',
},
{
value: 'steam-video-games:recommendationCount:asc',
label: 'Least Recommended',
},
],
}),
instantsearch.widgets.searchBox({container: "#searchbox"}),
instantsearch.widgets.configure({hitsPerPage: 8}),
instantsearch.widgets.hits({
container: "#hits",
templates: {
item: `
<div>
<div class="hit-name">
{{#helpers.highlight}}{"attribute": "name"}{{/helpers.highlight}}
</div>
</div>
`}
})]);
search.start();
</script>
</html>

80
npins/default.nix Normal file
View file

@ -0,0 +1,80 @@
# Generated by npins. Do not modify; will be overwritten regularly
let
data = builtins.fromJSON (builtins.readFile ./sources.json);
version = data.version;
mkSource =
spec:
assert spec ? type;
let
path =
if spec.type == "Git" then
mkGitSource spec
else if spec.type == "GitRelease" then
mkGitSource spec
else if spec.type == "PyPi" then
mkPyPiSource spec
else if spec.type == "Channel" then
mkChannelSource spec
else
builtins.throw "Unknown source type ${spec.type}";
in
spec // { outPath = path; };
mkGitSource =
{
repository,
revision,
url ? null,
hash,
branch ? null,
...
}:
assert repository ? type;
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
# In the latter case, there we will always be an url to the tarball
if url != null then
(builtins.fetchTarball {
inherit url;
sha256 = hash; # FIXME: check nix version & use SRI hashes
})
else
assert repository.type == "Git";
let
urlToName =
url: rev:
let
matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url;
short = builtins.substring 0 7 rev;
appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else "";
in
"${if matched == null then "source" else builtins.head matched}${appendShort}";
name = urlToName repository.url revision;
in
builtins.fetchGit {
url = repository.url;
rev = revision;
inherit name;
# hash = hash;
};
mkPyPiSource =
{ url, hash, ... }:
builtins.fetchurl {
inherit url;
sha256 = hash;
};
mkChannelSource =
{ url, hash, ... }:
builtins.fetchTarball {
inherit url;
sha256 = hash;
};
in
if version == 3 then
builtins.mapAttrs (_: mkSource) data.pins
else
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"

11
npins/sources.json Normal file
View file

@ -0,0 +1,11 @@
{
"pins": {
"nixpkgs": {
"type": "Channel",
"name": "nixpkgs-unstable",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre656183.453402b94f39/nixexprs.tar.xz",
"hash": "16lpbplz7f5ximc7j10qimr043xa89lx470ycb150529828y21wm"
}
},
"version": 3
}

25
optionsJSON.nix Normal file
View file

@ -0,0 +1,25 @@
{ lib, stdenv, jq, nixosOptionsDoc, nixpkgs }: let
# NB: This file describes the Nixpkgs manual, which happens to use module
# docs infra originally developed for NixOS.
optionsDoc = nixosOptionsDoc {
inherit (lib.evalModules {
modules = import "${nixpkgs}/nixos/modules/module-list.nix" ++ [ { nixpkgs.hostPlatform = "x86_64-linux"; } ];
class = "nixpkgsConfig";
}) options;
# documentType = "none";
# transformOptions = opt: opt;
};
in stdenv.mkDerivation {
name = "options.json";
src = nixpkgs;
buildInputs = [ jq ];
buildPhase = ''
jq -e 'to_entries | map(.key as $k | .value + {"id":($k | gsub("[^a-zA-Z0-9-_]"; "-")), "name":$k})' ${optionsDoc.optionsJSON}/share/doc/nixos/options.json > $out
'';
dontUnpack = true;
dontBuild = false;
doCheck = false;
}

18
packagesJSON.nix Normal file
View file

@ -0,0 +1,18 @@
{ stdenv
, nixpkgs
, jq
, lix
}: stdenv.mkDerivation {
name = "packages.json";
src = nixpkgs;
buildInputs = [ jq lix ];
buildPhase = ''
NIX_STATE_DIR=$TMPDIR NIX_PATH= nix-env -f $src -qa --meta --json --show-trace --arg config "import $src/pkgs/top-level/packages-config.nix" | jq -e 'to_entries | map(.key as $k | .value + {"id":($k | gsub("[^a-zA-Z0-9-_]"; "-")), "name":$k})' > $out
'';
dontUnpack = true;
dontBuild = false;
doCheck = false;
}

10
shell.nix Normal file
View file

@ -0,0 +1,10 @@
let
sources = import ./npins;
pkgs = import sources.nixpkgs { };
in pkgs.mkShell {
buildInputs = with pkgs; [
npins
python3
python3Packages.meilisearch
];
}

8
updateIndex/default.nix Normal file
View file

@ -0,0 +1,8 @@
{ python3Packages
, writers
}: writers.writePython3Bin "updateIndex" {
flakeIgnore = [ "E501" ];
libraries = with python3Packages; [
meilisearch
];
} (builtins.readFile ./default.py)

57
updateIndex/default.py Normal file
View file

@ -0,0 +1,57 @@
import meilisearch
import argparse
import json
import time
import os
def meiliTaskStatus(task_uid: int, verbose: bool):
taskInfo = client.get_task(task_uid)
while taskInfo.status != "succeeded" and taskInfo.status != "canceled" and taskInfo.status != "failed":
# no need to hammer the api
time.sleep(3)
taskInfo = client.get_task(task_uid)
if verbose:
print(taskInfo)
else:
if taskInfo.status == "succeeded":
print(f"job {taskInfo.type} for {taskInfo.index_uid} ({taskInfo.uid}) succeeded")
elif taskInfo.status == "canceled":
print(f"job {taskInfo.type} for {taskInfo.index_uid} ({taskInfo.uid}) was canceled")
elif taskInfo.status == "failed":
print(f"job {taskInfo.type} for {taskInfo.index_uid} ({taskInfo.uid}) failed")
else:
raise ValueError(f"unexpected job task type for: {taskInfo}")
# todo: error and figure out a sensible default via envrc and maybe a local instance with something like devenv
meiliInstance = os.getenv("MEILI_INSTANCE")
if meiliInstance is None:
meiliInstance = "https://meilisearch.aq0.de/"
parser = argparse.ArgumentParser(
prog='updateIndex',
description='Updates meilisearch indexs, inteded to be be used for search.forkos.org'
)
parser.add_argument('documentsJsonFile')
parser.add_argument('-k', '--kind', choices=['packages', 'options'], default='packages')
parser.add_argument('-r', '--release', choices=['rolling', 'stable'], default='rolling')
parser.add_argument('-v', '--verbose', action='store_true')
args = parser.parse_args()
client = meilisearch.Client(meiliInstance)
indexName = f"{args.kind}-{args.release}"
with open(args.documentsJsonFile) as json_file:
documents = json.load(json_file)
if args.kind == "packages":
documentResponse = client.index(indexName).add_documents(documents)
indexResponse = client.index(indexName).update_filterable_attributes(['meta'])
meiliTaskStatus(documentResponse.task_uid, args.verbose)
meiliTaskStatus(indexResponse.task_uid, args.verbose)
elif args.kind == "options":
documentResponse = client.index(indexName).add_documents(documents)
meiliTaskStatus(documentResponse.task_uid, args.verbose)
else:
raise ValueError("invalid value for type kind")