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

Windows support #640

Merged
merged 42 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
73d6bd1
Fix pip install -e . on Windows
asmeurer Sep 5, 2023
cea6670
Use None for permissions when on Windows
asmeurer Sep 6, 2023
a7b3338
Use the Python API for alembic rather than a subprocess
asmeurer Sep 6, 2023
d2d0f29
Use python -m conda_lock
asmeurer Sep 12, 2023
0dc444c
Lower the batch size for updating packages from 1000 to 990
asmeurer Sep 15, 2023
58fd321
Use a pure Python equivalent of du on Windows
asmeurer Sep 18, 2023
48787eb
Fix missing parenthesis
asmeurer Sep 25, 2023
99e65ae
Fix the worker for Windows
asmeurer Sep 25, 2023
cb2dffb
Use posixpath to construct URLs
asmeurer Sep 25, 2023
407f55b
Fix login not working consistently on Windows
asmeurer Oct 5, 2023
3f4b340
Fix test_action_decorator to work on Windows
asmeurer Oct 5, 2023
f334d31
Fix test_action_decorator on Windows
asmeurer Oct 5, 2023
1107907
Skip test_set_conda_prefix_permissions on Windows
asmeurer Oct 5, 2023
7dd17f1
Fix du() on Mac and Windows to return bytes instead of blocks
asmeurer Oct 5, 2023
129b8b0
Add Windows to CI
asmeurer Oct 9, 2023
9e1ddef
Run pre-commit
asmeurer Oct 9, 2023
eaf6d20
Try using python -m on CI
asmeurer Oct 9, 2023
0bf528a
Revert "Try using python -m on CI"
asmeurer Oct 9, 2023
e610a5a
Trigger CI
asmeurer Oct 10, 2023
5b395c6
Run tests in verbose mode
asmeurer Oct 10, 2023
ea643d7
Add an option to not redirect stderr in context.run
asmeurer Oct 10, 2023
b908389
Don't capture stderr with conda env export --json
asmeurer Oct 10, 2023
c3d1327
Print conda-store server address to the terminal when using --standalone
asmeurer Oct 11, 2023
af302b8
Use "localhost" instead of "127.0.0.1" for consistency with the docs
asmeurer Oct 11, 2023
362266c
Add a note to the docs that Docker image creation only works on Linux
asmeurer Oct 11, 2023
71d015c
Document that filesystem permissions options aren't supported on Windows
asmeurer Oct 11, 2023
5913460
Fix a formatting issue in the docs
asmeurer Oct 11, 2023
370bb8b
Don't document a config file for using --standalone
asmeurer Oct 11, 2023
90b6650
Add FAQ entry for long paths on Windows
asmeurer Oct 11, 2023
2b05f8f
Add a basic test for disk_usage()/du()
asmeurer Oct 11, 2023
e0b9508
Run black
asmeurer Oct 11, 2023
c09d03d
Document that there are different environment files for Mac and Windows
asmeurer Oct 11, 2023
b4c687d
Fix filenames
asmeurer Oct 11, 2023
961c006
Fix filename
asmeurer Oct 11, 2023
3232bbe
Account for the size of the directory itself (which is large on Linux)
asmeurer Oct 12, 2023
f0fae40
Update Windows conda env file
Oct 22, 2023
6f79d5d
Try force reinstalling Python on Windows
asmeurer Oct 10, 2023
0f75d48
Do not run pytest in verbose mode
Oct 22, 2023
526a556
Do not print address and port when starting server
Oct 22, 2023
a71f24d
Simplify du
Oct 22, 2023
0622125
Use `sys.platform` and `Optional` to be consistent
Oct 22, 2023
0ad9f5a
Revert incorrect comment change
Oct 22, 2023
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
15 changes: 13 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ jobs:
name: "unit-test conda-store-server"
strategy:
matrix:
# cannot run on windows due to needing fake-chroot for conda-docker
os: ["ubuntu", "macos"]
os: ["ubuntu", "macos", "windows"]
include:
- os: ubuntu
environment-file: conda-store-server/environment-dev.yaml
- os: macos
environment-file: conda-store-server/environment-macos-dev.yaml
- os: windows
environment-file: conda-store-server/environment-windows-dev.yaml
nkaretnikov marked this conversation as resolved.
Show resolved Hide resolved
runs-on: ${{ matrix.os }}-latest
defaults:
run:
Expand All @@ -41,6 +42,16 @@ jobs:
environment-file: ${{ matrix.environment-file }}
miniforge-version: latest

# This fixes a "DLL not found" issue importing ctypes from the hatch env
- name: Reinstall Python 3.10 on Windows runner
uses: nick-fields/[email protected]
with:
timeout_minutes: 9999
max_attempts: 6
command:
conda install --channel=conda-forge --quiet --yes python=${{ matrix.python }}
if: matrix.os == 'windows'

- name: "Linting Checks 🧹"
run: |
hatch env run -e dev lint
Expand Down
7 changes: 5 additions & 2 deletions conda-store-server/conda_store_server/action/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,23 @@ class ActionContext:
def __init__(self):
self.id = str(uuid.uuid4())
self.stdout = io.StringIO()
self.stderr = io.StringIO()
self.log = logging.getLogger(f"conda_store_server.action.{self.id}")
self.log.propagate = False
self.log.addHandler(logging.StreamHandler(stream=self.stdout))
self.log.setLevel(logging.INFO)
self.result = None
self.artifacts = {}

def run(self, *args, **kwargs):
def run(self, *args, redirect_stderr=True, **kwargs):
result = subprocess.run(
*args,
**kwargs,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stderr=subprocess.STDOUT if redirect_stderr else subprocess.PIPE,
encoding="utf-8",
)
self.stdout.write(result.stdout)
if not redirect_stderr:
self.stderr.write(result.stderr)
return result
Copy link
Contributor Author

Choose a reason for hiding this comment

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

See #559 (comment) for context on redirects.

Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ def action_generate_conda_export(
"--json",
]

result = context.run(command, check=True)
result = context.run(command, check=True, redirect_stderr=False)
if result.stderr:
context.log.warning(f"conda env export stderr: {result.stderr}")
return json.loads(result.stdout)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import pathlib
import sys
import typing

from conda_store_server import action
Expand All @@ -16,7 +17,9 @@ def action_install_lockfile(
json.dump(conda_lock_spec, f)

command = [
"conda-lock",
sys.executable,
"-m",
"conda_lock",
"install",
"--validate-platform",
"--log-level",
Expand Down
9 changes: 6 additions & 3 deletions conda-store-server/conda_store_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,21 +302,24 @@ def _default_celery_results_backend(self):
)

default_uid = Integer(
os.getuid(),
None if sys.platform == "win32" else os.getuid(),
Copy link
Member

Choose a reason for hiding this comment

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

I see sys.platform == "win32" in many places. Should we have a constant that we create for this? Since much of our code only has a special path for Windows?

Copy link
Member

@costrouc costrouc Oct 24, 2023

Choose a reason for hiding this comment

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

More I think about it I don't think this is necessary to have a constant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There was a constant in Aaron's original code. Well, two constants, in different files. Since you need to share this between multiple files, you'd have to put it in __init__ to avoid import issues. But we don't do this for any other platforms, so I decided to simply use sys.platform, as everywhere else, to avoid introducing new entities.

help="default uid to assign to built environments",
config=True,
allow_none=True,
)

default_gid = Integer(
os.getgid(),
None if sys.platform == "win32" else os.getgid(),
help="default gid to assign to built environments",
config=True,
allow_none=True,
)

default_permissions = Unicode(
"775",
None if sys.platform == "win32" else "775",
help="default file permissions to assign to built environments",
config=True,
allow_none=True,
)

default_docker_base_image = Union(
Expand Down
7 changes: 4 additions & 3 deletions conda-store-server/conda_store_server/dbutil.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
from contextlib import contextmanager
from subprocess import check_call
from tempfile import TemporaryDirectory

from alembic import command
Expand Down Expand Up @@ -78,6 +77,8 @@ def upgrade(db_url, revision="head"):
current_table_names = set(inspect(engine).get_table_names())

with _temp_alembic_ini(db_url) as alembic_ini:
alembic_cfg = Config(alembic_ini)

if (
"alembic_version" not in current_table_names
and len(current_table_names) > 0
Expand All @@ -86,10 +87,10 @@ def upgrade(db_url, revision="head"):
# we stamp the revision at the first one, that introduces the alembic revisions.
# I chose the leave the revision number hardcoded as it's not something
# dynamic, not something we want to change, and tightly related to the codebase
command.stamp(Config(alembic_ini), "48be4072fe58")
command.stamp(alembic_cfg, "48be4072fe58")
# After this point, whatever is in the database, Alembic will
# believe it's at the first revision. If there are more upgrades/migrations
# to run, they'll be at the next step :

# run the upgrade.
check_call(["alembic", "-c", alembic_ini, "upgrade", revision])
command.upgrade(config=alembic_cfg, revision=revision)
5 changes: 3 additions & 2 deletions conda-store-server/conda_store_server/orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,10 +513,11 @@ def update_packages(self, db, subdirs=None):
package_builds[package_key].append(new_package_build_dict)
logger.info("CondaPackageBuild objects created")

batch_size = 1000
# sqlite3 has a max expression depth of 1000
batch_size = 990
Copy link
Member

Choose a reason for hiding this comment

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

I bet there is an interesting story here? Is this only something that showed up on 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.

I haven't seen this personally, but it's mentioned in one of the Aaron's commits:

Lower the batch size for updating packages from 1000 to 990
1000 hits the sqlite3 max expression depth and results in an error.

Seems harmless, so I decided to keep it.

all_package_keys = list(package_builds.keys())
for i in range(0, len(all_package_keys), batch_size):
logger.info(f"handling subset at index {i} (batch size {batch_size}")
logger.info(f"handling subset at index {i} (batch size {batch_size})")
subset_keys = all_package_keys[i : i + batch_size]

# retrieve the parent packages for the subset
Expand Down
12 changes: 6 additions & 6 deletions conda-store-server/conda_store_server/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,20 +194,20 @@ class Settings(BaseModel):
metadata={"global": True},
)

default_uid: int = Field(
os.getuid(),
default_uid: Optional[int] = Field(
None if sys.platform == "win32" else os.getuid(),
description="default uid to assign to built environments",
metadata={"global": True},
)

default_gid: int = Field(
os.getgid(),
default_gid: Optional[int] = Field(
None if sys.platform == "win32" else os.getgid(),
description="default gid to assign to built environments",
metadata={"global": True},
)

default_permissions: str = Field(
"775",
default_permissions: Optional[str] = Field(
None if sys.platform == "win32" else "775",
description="default file permissions to assign to built environments",
metadata={"global": True},
)
Expand Down
7 changes: 4 additions & 3 deletions conda-store-server/conda_store_server/server/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import posixpath
import sys

import conda_store_server
Expand Down Expand Up @@ -198,9 +199,9 @@ def trim_slash(url):
app = FastAPI(
title="conda-store",
version=__version__,
openapi_url=os.path.join(self.url_prefix, "openapi.json"),
docs_url=os.path.join(self.url_prefix, "docs"),
redoc_url=os.path.join(self.url_prefix, "redoc"),
openapi_url=posixpath.join(self.url_prefix, "openapi.json"),
Copy link
Member

Choose a reason for hiding this comment

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

I'll admit using os.path... for urls is a hack and I should have used something like yarl for example. Thoughts? Since url paths are not the same as file paths? Maybe posixpath is the right thing to use here. It certainly seems better than os.path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A separate entity for URLs would be better, yes. But I'm not going to block this PR just because of this issue, since everything seems to work fine.

docs_url=posixpath.join(self.url_prefix, "docs"),
redoc_url=posixpath.join(self.url_prefix, "redoc"),
contact={
"name": "Quansight",
"url": "https://quansight.com",
Expand Down
4 changes: 3 additions & 1 deletion conda-store-server/conda_store_server/server/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,9 @@ async def post_login_method(
samesite="strict",
domain=self.cookie_domain,
# set cookie to expire at same time as jwt
max_age=(authentication_token.exp - datetime.datetime.utcnow()).seconds,
max_age=int(
(authentication_token.exp - datetime.datetime.utcnow()).total_seconds()
),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

A fun bug 😄 glad we got to the bottom of this one.

)
return response

Expand Down
3 changes: 2 additions & 1 deletion conda-store-server/conda_store_server/storage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
import os
import posixpath
import shutil

import minio
Expand Down Expand Up @@ -223,7 +224,7 @@ def get(self, key):
return f.read()

def get_url(self, key):
return os.path.join(self.storage_url, key)
return posixpath.join(self.storage_url, key)

def delete(self, db, build_id, key):
filename = os.path.join(self.storage_path, key)
Expand Down
35 changes: 33 additions & 2 deletions conda-store-server/conda_store_server/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,44 @@ def chdir(directory: pathlib.Path):
os.chdir(current_directory)


def du(path):
"""
Pure Python equivalent of du -sb
Based on https://stackoverflow.com/a/55648984/161801
"""
if os.path.islink(path) or os.path.isfile(path):
return os.lstat(path).st_size
nbytes = 0
seen = set()
for dirpath, dirnames, filenames in os.walk(path):
nbytes += os.lstat(dirpath).st_size
for f in filenames:
fp = os.path.join(dirpath, f)
st = os.lstat(fp)
if st.st_ino in seen:
continue
seen.add(st.st_ino) # adds inode to seen list
nbytes += st.st_size # adds bytes to total
for d in dirnames:
dp = os.path.join(dirpath, d)
if os.path.islink(dp):
nbytes += os.lstat(dp).st_size
return nbytes


def disk_usage(path: pathlib.Path):
if sys.platform == "darwin":
cmd = ["du", "-sAB1", str(path)]
else:
elif sys.platform == "linux":
cmd = ["du", "-sb", str(path)]
else:
return str(du(path))
Copy link
Member

Choose a reason for hiding this comment

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

Good to see the fallback is this approach 👍 If they are using cygwin or https://learn.microsoft.com/en-us/sysinternals/downloads/du it may still be possible they have du.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We've already looked at du from sysinternals. Besides being a separate tool, it also produces slightly different results. And the output format is also different. As for cygwin, the context for this work is to be able to use native Windows tooling.


return subprocess.check_output(cmd, encoding="utf-8").split()[0]
output = subprocess.check_output(cmd, encoding="utf-8").split()[0]
if sys.platform == "darwin":
# mac du does not have the -b option to return bytes
output = str(int(output) * 512)
Copy link
Member

Choose a reason for hiding this comment

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

Why 512? This is a weird size since it is a half a kb.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A comment from a discussion on the original PR: "Note that the numbers will be off a little bit (by less than 512) on Mac because du rounds up to the nearest multiple of 512 when converting bytes to blocks."

I'm still not certain that macOS code is correct here, but it does pass this new test (where we check all three platforms), so I'm going to keep this and will take a look later.

return output
nkaretnikov marked this conversation as resolved.
Show resolved Hide resolved


@contextlib.contextmanager
Expand Down
11 changes: 10 additions & 1 deletion conda-store-server/conda_store_server/worker/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,18 @@ def start(self):
argv = [
"worker",
"--loglevel=INFO",
"--beat",
]

# The default Celery pool requires this on Windows. See
# https://stackoverflow.com/questions/37255548/how-to-run-celery-on-windows
Copy link
Member

Choose a reason for hiding this comment

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

Great glad this fix was minimal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's still not clear whether it means that Celery works as intended, since it's not officially supported on Windows. But it does run, so we'll just use this for now.

if sys.platform == "win32":
os.environ.setdefault("FORKED_BY_MULTIPROCESSING", "1")
else:
# --beat does not work on Windows
argv += [
"--beat",
]

if self.concurrency:
argv.append(f"--concurrency={self.concurrency}")

Expand Down
59 changes: 59 additions & 0 deletions conda-store-server/environment-windows-dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: conda-store-server-dev
nkaretnikov marked this conversation as resolved.
Show resolved Hide resolved
channels:
- conda-forge
- microsoft
- nodefaults
dependencies:
- python ==3.10
# conda builds
- conda ==23.5.2
- python-docker
- conda-pack
- conda-lock >=1.0.5
- mamba
- conda-package-handling
# web server
- celery
- flower
- redis-py
- sqlalchemy<=1.4.47
- psycopg2
- pymysql
- requests
- uvicorn
- fastapi
- pydantic < 2.0
- pyyaml
- traitlets
- yarl
- pyjwt
- filelock
- itsdangerous
- jinja2
- python-multipart
- alembic
# artifact storage
- minio
# CLI
- typer

# dev dependencies
- aiohttp>=3.8.1
- hatch
- pytest
- pytest-celery
- pytest-mock
- black ==22.3.0
- flake8
- ruff
- sphinx
- myst-parser
- sphinx-panels
- sphinx-copybutton
- pydata-sphinx-theme
- playwright
- docker-compose
- pip

- pip:
- pytest-playwright
6 changes: 4 additions & 2 deletions conda-store-server/hatch_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ def initialize(self, version: str, build_data: Dict[str, Any]) -> None:
# main.js to enable easy configuration see
# conda_store_server/server/templates/conda-store-ui.html
# for global variable set
with (source_directory / "main.js").open("r") as source_f:
with (source_directory / "main.js").open("r", encoding="utf-8") as source_f:
content = source_f.read()
content = re.sub(
'"MISSING_ENV_VAR"', "GLOBAL_CONDA_STORE_STATE", content
)
with (destination_directory / "main.js").open("w") as dest_f:
with (destination_directory / "main.js").open(
"w", encoding="utf-8"
) as dest_f:
dest_f.write(content)
Loading
Loading