Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sith compatible with windows #863

Draft
wants to merge 3 commits into
base: taiste
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 206 additions & 18 deletions core/management/commands/install_xapian.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,42 @@
#
#

import hashlib
import multiprocessing
import os
import platform
import shutil
import subprocess
import sys
import tarfile
from dataclasses import dataclass
from pathlib import Path
from typing import Self

import tomli
from django.core.management.base import BaseCommand, CommandParser
import urllib3
from django.core.management.base import BaseCommand, CommandParser, OutputWrapper
from urllib3.response import HTTPException


@dataclass
class XapianSpec:
version: str
core_sha1: str
bindings_sha1: str

@classmethod
def from_pyproject(cls) -> Self:
with open(
Path(__file__).parent.parent.parent.parent / "pyproject.toml", "rb"
) as f:
pyproject = tomli.load(f)
spec = pyproject["tool"]["xapian"]
return cls(
version=spec["version"],
core_sha1=spec["core-sha1"],
bindings_sha1=spec["bindings-sha1"],
)


class Command(BaseCommand):
Expand All @@ -39,34 +69,192 @@ def _current_version(self) -> str | None:
return None
return xapian.version_string()

def _desired_version(self) -> str:
with open(
Path(__file__).parent.parent.parent.parent / "pyproject.toml", "rb"
) as f:
pyproject = tomli.load(f)
return pyproject["tool"]["xapian"]["version"]

def handle(self, *args, force: bool, **options):
if not os.environ.get("VIRTUAL_ENV", None):
self.stdout.write(
"No virtual environment detected, this command can't be used"
)
return

desired = self._desired_version()
if desired == self._current_version():
desired = XapianSpec.from_pyproject()
if desired.version == self._current_version():
if not force:
self.stdout.write(
f"Version {desired} is already installed, use --force to re-install"
f"Version {desired.version} is already installed, use --force to re-install"
)
return
self.stdout.write(f"Version {desired} is already installed, re-installing")
self.stdout.write(
f"Installing xapian version {desired} at {os.environ['VIRTUAL_ENV']}"
self.stdout.write(
f"Version {desired.version} is already installed, re-installing"
)
XapianInstaller(desired, self.stdout, self.stderr).run()
self.stdout.write("Installation success")


class XapianInstaller:
def __init__(
self,
spec: XapianSpec,
stdout: OutputWrapper,
stderr: OutputWrapper,
):
self._version = spec.version
self._core_sha1 = spec.core_sha1
self._bindings_sha1 = spec.bindings_sha1

self._stdout = stdout
self._stderr = stderr
self._virtual_env = os.environ.get("VIRTUAL_ENV", None)

if not self._virtual_env:
raise RuntimeError("You are not inside a virtual environment")
self._virtual_env = Path(self._virtual_env)

self._dest_dir = Path(self._virtual_env) / "packages"
self._core = f"xapian-core-{self._version}"
self._bindings = f"xapian-bindings-{self._version}"

@property
def _is_windows(self) -> bool:
return platform.system() == "Windows"

def _util_download(self, url: str, dest: Path, sha1_hash: str) -> None:
resp = urllib3.request("GET", url)
if resp.status != 200:
raise HTTPException(f"Could not download {url}")
if hashlib.sha1(resp.data).hexdigest() != sha1_hash:
raise ValueError(f"File downloaded from {url} is compromised")
with open(dest, "wb") as f:
f.write(resp.data)

def _setup_env(self):
os.environ.update(
{
"CPATH": "",
"LIBRARY_PATH": "",
"CFLAGS": "",
"LDFLAGS": "",
"CCFLAGS": "",
"CXXFLAGS": "",
"CPPFLAGS": "",
}
)

def _prepare_dest_folder(self):
shutil.rmtree(self._dest_dir, ignore_errors=True)
self._dest_dir.mkdir(parents=True, exist_ok=True)

def _download(self):
self._stdout.write("Downloading source…")

core = self._dest_dir / f"{self._core}.tar.xz"
bindings = self._dest_dir / f"{self._bindings}.tar.xz"
self._util_download(
f"https://oligarchy.co.uk/xapian/{self._version}/{self._core}.tar.xz",
core,
self._core_sha1,
)
self._util_download(
f"https://oligarchy.co.uk/xapian/{self._version}/{self._bindings}.tar.xz",
bindings,
self._bindings_sha1,
)
self._stdout.write("Extracting source …")
with tarfile.open(core) as tar:
tar.extractall(self._dest_dir)
with tarfile.open(bindings) as tar:
tar.extractall(self._dest_dir)

os.remove(core)
os.remove(bindings)

def _install(self):
self._stdout.write("Installing Xapian-core…")
def configure() -> list[str]:
if self._is_windows:
return ["sh", "configure"]
return ["./configure"]
def enable_static() -> list[str]:
if self._is_windows:
return ["--enable-shared", "--disable-static"]
return []

# Make sure that xapian finds the correct executable
os.environ["PYTHON3"] = str(Path(sys.executable).as_posix())

subprocess.run(
[str(Path(__file__).parent / "install_xapian.sh"), desired],
[*configure(), "--prefix", str(self._virtual_env.as_posix()), *enable_static(),],
env=dict(os.environ),
check=True,
)
self.stdout.write("Installation success")
cwd=self._dest_dir / self._core,
check=False,
shell=self._is_windows,
).check_returncode()
subprocess.run(
[
"make",
"-j",
str(multiprocessing.cpu_count()),
],
Comment on lines +192 to +196
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Si tu utilises make, il faut rajouter dans la doc la manière d'installer ça sur Windows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alors justement, j'y suis pas encore
Là j'ai seulement fait la première étape : convertir le script

La seconde étape ce sera de télécharger mingw dans un dossier du projet quand t'es sur windows, histoire que tu puisse compiler sans devoir expliquer à quelqu'un c'est quoi un path

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mouais. Je sais pas trop si c'est souhaitable d'installer mingw sans demander. Ca reste relativement pas trop douloureux à installer, mingw, donc en vrai fournir des instructions d'installation, ça me parait ok.

Mais bon, je te laisse faire, je regarde les idées que t'implémenteras.

env=dict(os.environ),
cwd=self._dest_dir / self._core,
check=False,
shell=self._is_windows,
).check_returncode()
subprocess.run(
["make", "install"],
env=dict(os.environ),
cwd=self._dest_dir / self._core,
check=False,
shell=self._is_windows,

).check_returncode()


self._stdout.write("Installing Xapian-bindings")
subprocess.run(
[
*configure(),
"--prefix",
str(self._virtual_env.as_posix()),
"--with-python3",
f"XAPIAN_CONFIG={(self._virtual_env / 'bin'/'xapian-config').as_posix()}",
*enable_static(),
],
env=dict(os.environ),
cwd=self._dest_dir / self._bindings,
check=False,
shell=self._is_windows,
).check_returncode()
subprocess.run(
[
"make",
"-j",
str(multiprocessing.cpu_count()),
],
env=dict(os.environ),
cwd=self._dest_dir / self._bindings,
check=False,
shell=self._is_windows,
).check_returncode()
subprocess.run(
["make", "install"],
env=dict(os.environ),
cwd=self._dest_dir / self._bindings,
check=False,
shell=self._is_windows,
).check_returncode()

def _post_clean(self):
shutil.rmtree(self._dest_dir, ignore_errors=True)

def _test(self):
subprocess.run(
[sys.executable, "-c", "import xapian"], check=False, shell=self._is_windows,
).check_returncode()

def run(self):
self._setup_env()
self._prepare_dest_folder()
self._download()
self._install()
self._post_clean()
self._test()
47 changes: 0 additions & 47 deletions core/management/commands/install_xapian.sh

This file was deleted.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ default-groups = ["dev", "tests", "docs"]

[tool.xapian]
version = "1.4.25"
core-sha1 = "e2b4b4cf6076873ec9402cab7b9a3b71dcf95e20"
bindings-sha1 = "782f568d2ea3ca751c519a2814a35c7dc86df3a4"
Comment on lines +87 to +88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ca sera quoi, le processus pour changer les hashs ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faut le faire manuellement quand tu upgrade, je pense qu'un tutoriel ce serait pas mal.
Ou alors je peux le faire moi même dans le script mais ça commence rapidement à être de la magie noir

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mets-le dans le tuto. Ca devrait suffire, je pense. Surtout que c'est pas quelque chose qu'on fait souvent.


[tool.ruff]
output-format = "concise" # makes ruff error logs easier to read
Expand Down
7 changes: 4 additions & 3 deletions staticfiles/processors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import logging
import subprocess
import platform
from dataclasses import dataclass
from hashlib import sha1
from itertools import chain
Expand Down Expand Up @@ -94,7 +95,7 @@ class JSBundler:
@staticmethod
def compile():
"""Bundle js files with the javascript bundler for production."""
process = subprocess.Popen(["npm", "run", "compile"])
process = subprocess.Popen(["npm", "run", "compile"], shell=platform.system() == "Windows")
process.wait()
if process.returncode:
raise RuntimeError(f"Bundler failed with returncode {process.returncode}")
Expand All @@ -103,7 +104,7 @@ def compile():
def runserver() -> subprocess.Popen:
"""Bundle js files automatically in background when called in debug mode."""
logging.getLogger("django").info("Running javascript bundling server")
return subprocess.Popen(["npm", "run", "serve"])
return subprocess.Popen(["npm", "run", "serve"], shell=platform.system() == "Windows")

@staticmethod
def get_manifest() -> JSBundlerManifest:
Expand Down Expand Up @@ -197,4 +198,4 @@ def compile(cls):
with open(out, "w") as f:
_ = f.write(schema)

subprocess.run(["npx", "openapi-ts"], check=True)
subprocess.run(["npx", "openapi-ts"], check=True, shell=platform.system() == "Windows")
6 changes: 3 additions & 3 deletions vite.config.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// biome-ignore lint/correctness/noNodejsModules: this is backend side
import { parse, resolve } from "node:path";
import { parse, resolve, sep } from "node:path";
import inject from "@rollup/plugin-inject";
import { glob } from "glob";
import { type AliasOptions, type UserConfig, defineConfig } from "vite";
Expand Down Expand Up @@ -31,7 +31,7 @@ function getAliases(): AliasOptions {
function getRelativeAssetPath(path: string): string {
let relativePath: string[] = [];
const fullPath = parse(path);
for (const dir of fullPath.dir.split("/").reverse()) {
for (const dir of fullPath.dir.split(sep).reverse()) {
if (dir === "bundled") {
break;
}
Expand All @@ -40,7 +40,7 @@ function getRelativeAssetPath(path: string): string {
// We collected folders in reverse order, we put them back in the original order
relativePath = relativePath.reverse();
relativePath.push(fullPath.name);
return relativePath.join("/");
return relativePath.join(sep);
}

// biome-ignore lint/style/noDefaultExport: this is recommended by documentation
Expand Down
Loading