Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,8 @@ target/

# Ipython Notebook
.ipynb_checkpoints

# Offline local-install bundle artifacts (never commit wheels or installers)
local-install/whls/
local-install/*.exe
pytentiostat-env/
21 changes: 11 additions & 10 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ci:
submodules: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v6.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
Expand All @@ -21,45 +21,46 @@ repos:
- id: check-toml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 24.4.2
rev: 26.3.1
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
rev: 7.3.0
hooks:
- id: flake8
- repo: https://github.com/pycqa/isort
rev: 5.13.2
rev: 9.0.0a3
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/kynan/nbstripout
rev: 0.7.1
rev: 0.9.1
hooks:
- id: nbstripout
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v6.0.0
hooks:
- id: no-commit-to-branch
name: Prevent Commit to Main Branch
args: ["--branch", "main"]
stages: [pre-commit]
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
rev: v2.4.2
hooks:
- id: codespell
additional_dependencies:
- tomli
# prettier - multi formatter for .json, .yml, and .md files
- repo: https://github.com/pre-commit/mirrors-prettier
rev: f12edd9c7be1c20cfa42420fd0e6df71e42b51ea # frozen: v4.0.0-alpha.8
rev: v4.0.0-alpha.8
hooks:
- id: prettier
additional_dependencies:
- "prettier@^3.2.4"
types: [yaml, markdown, json]
# docformatter - PEP 257 compliant docstring formatter
- repo: https://github.com/s-weigand/docformatter
rev: 5757c5190d95e5449f102ace83df92e7d3b06c6c
- repo: https://github.com/PyCQA/docformatter
rev: v1.7.8
hooks:
- id: docformatter
additional_dependencies: [tomli]
Expand Down
69 changes: 50 additions & 19 deletions local-install/instructions.rst
Original file line number Diff line number Diff line change
@@ -1,23 +1,54 @@
.. instructions for creating a USB that can install pytentiostat
.. instructions for creating a USB that can install pytentiostat offline

=============================================================================
Instructions for installing pytentiostat onto a *Windows* computer with a USB
Installing pytentiostat onto an offline *Windows* computer with a USB
=============================================================================

- First, copy the entire top level directory onto a USB flash drive.
- **Important:** Download the Python 3.8 installer and required wheel files onto the USB. \
Copy the installer to the folder 'local_install' and the wheels to its subdirectory, \
'whls'.

**Note:** To obtain a copy of the installer and wheel files, contact the \
development team through Github or email. (https://github.com/juami/pytentiostat)
- Next, plug in the flash drive to the desired computer and copy \
pytentiostat onto the computer.
- Then find the file called 'install_from_local' in the scripts folder \
and double click it.
- Follow the prompts on the screen to install python 3.8. Be sure to select \
'add Python to PATH' at the bottom of the first screen.
- It will take a couple of minutes to install all the packages. If everything \
was successful, you will see no errors and the bottom of the command window \
will say 'press any key to continue...'
- Refer to the main documentation in the 'docs' folder to start running experiments.
These instructions create a USB flash drive that can install pytentiostat on a
lab computer that has **no internet access**. The wheels and the Python
installer are *not* stored in the repository -- you download them once on an
internet-connected machine using the helper script below.

Part 1 -- prepare the USB (on a machine **with** internet)
----------------------------------------------------------

#. Install Python 3.12 and copy (or ``git clone``) this repository onto the
machine.
#. From the top level of the repository, build the offline bundle::

python scripts/build_local_install.py

This builds the pytentiostat wheel and downloads every dependency wheel
into ``local-install/whls``.

**Note:** for the most reliable result, run this on the same operating
system and Python version as the target computer (Windows + Python 3.12 for
a typical lab machine). To build for a different platform, pass
``--platform win_amd64 --python-version 3.12``.

#. Download a Windows Python 3.12 installer from
https://www.python.org/downloads/windows/ and place the ``python-3.12*.exe``
file in the ``local-install`` folder. The installer script launches it
automatically if Python is missing on the target machine.
#. Copy the entire repository directory onto the USB flash drive.

Part 2 -- install pytentiostat (on the **offline** machine)
-----------------------------------------------------------

#. Plug in the USB flash drive and copy the pytentiostat directory onto the
computer.
#. Open the ``scripts`` folder and double-click ``install_from_local.bat``.
#. If Python is not already installed, the bundled installer launches first --
be sure to tick **"Add Python to PATH"** on the first screen. (If Python was
just installed, close the window, open a new terminal, and run the script
again so the updated ``PATH`` takes effect.)
#. The script creates a ``pytentiostat-env`` virtual environment and installs
pytentiostat and its dependencies from the bundled wheels. When it finishes
you will see a success message.
#. Activate the environment and launch the program::

pytentiostat-env\Scripts\activate
pytentiostat

Refer to the main documentation in the ``docs`` folder to start running
experiments.
29 changes: 29 additions & 0 deletions news/install-script.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
**Added:**

* ``scripts/build_local_install.py`` to build an offline install bundle
(pytentiostat wheel + dependency wheels) on an internet-connected machine
for use on an air-gapped lab computer.

**Changed:**

* Rewrote ``scripts/install_from_local.bat`` to install pytentiostat offline
from the bundled wheels into a virtual environment via
``pip install --no-index --find-links`` (Python 3.12, ``pip`` instead of the
removed ``setup.py install``), and updated ``local-install/instructions.rst``
to match.

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
143 changes: 143 additions & 0 deletions scripts/build_local_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env python
"""Build an offline install bundle for pytentiostat.

Run this on a machine **with** internet access to populate
``local-install/whls`` with the pytentiostat wheel and all of its
dependencies. The whole repository directory (now containing the wheels and,
optionally, a bundled Python installer) can then be copied to a USB flash
drive and used to install pytentiostat on an **offline** machine via
``scripts/install_from_local.bat``.

For the most reliable result, run this on the same OS/Python as the target
machine (Windows + Python 3.12 for a typical lab computer). To download wheels
for a *different* platform than the one you are on, pass ``--platform`` and
``--python-version`` (this forces wheel-only downloads).

Examples
--------
Build for the current machine::

python scripts/build_local_install.py

Build Windows wheels for Python 3.12 from another OS by adding::

--platform win_amd64 --python-version 3.12
"""

import argparse
import subprocess
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_DEST = REPO_ROOT / "local-install" / "whls"
DEFAULT_REQUIREMENTS = REPO_ROOT / "requirements" / "pip.txt"

# A recent Windows installer URL is printed as a reminder; the installer
# itself is intentionally not committed to the repository.
PYTHON_INSTALLER_HINT = (
"https://www.python.org/downloads/windows/ "
"(pick a 3.12.x 'Windows installer (64-bit)')"
)


def _run(cmd):
"""Run a subprocess command, echoing it first, and raise on
failure."""
print(" $ " + " ".join(str(part) for part in cmd))
subprocess.run(cmd, check=True)


def build_wheel(dest):
"""Build the pytentiostat wheel (without dependencies) into
``dest``."""
print("Building the pytentiostat wheel...")
_run(
[
sys.executable,
"-m",
"pip",
"wheel",
str(REPO_ROOT),
"--no-deps",
"--wheel-dir",
str(dest),
]
)


def download_dependencies(dest, requirements, platform, python_version):
"""Download all runtime dependency wheels into ``dest``."""
print("Downloading dependency wheels...")
cmd = [
sys.executable,
"-m",
"pip",
"download",
"--requirement",
str(requirements),
"--dest",
str(dest),
]
# Cross-platform downloads require pip to fetch pre-built wheels only.
if platform:
cmd += ["--only-binary=:all:", "--platform", platform]
if python_version:
cmd += ["--python-version", python_version]
_run(cmd)


def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--dest",
type=Path,
default=DEFAULT_DEST,
help="Directory to write wheels into (default: local-install/whls).",
)
parser.add_argument(
"--requirements",
type=Path,
default=DEFAULT_REQUIREMENTS,
help="Requirements file to download (default: requirements/pip.txt).",
)
parser.add_argument(
"--platform",
default=None,
help="Target wheel platform, e.g. win_amd64. Omit to use this "
"machine's platform.",
)
parser.add_argument(
"--python-version",
default=None,
help="Target Python version, e.g. 3.12. Omit to use this "
"interpreter's version.",
)
args = parser.parse_args(argv)

dest = args.dest.resolve()
dest.mkdir(parents=True, exist_ok=True)

build_wheel(dest)
download_dependencies(
dest, args.requirements, args.platform, args.python_version
)

wheels = sorted(p.name for p in dest.glob("*.whl"))
archives = sorted(p.name for p in dest.glob("*.tar.gz"))
print()
print(f"Done. {len(wheels) + len(archives)} package(s) staged in {dest}")
print()
print("Next steps to finish the USB bundle:")
print(
" 1. Download a Windows Python installer into 'local-install/'\n"
f" {PYTHON_INSTALLER_HINT}"
)
print(" 2. Copy the whole repository directory onto the USB flash drive.")
print(
" 3. On the offline machine, run " "'scripts/install_from_local.bat'."
)


if __name__ == "__main__":
main()
Loading
Loading