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

site-packages should be treated as read-only #117

Open
milahu opened this issue Jan 16, 2024 · 2 comments
Open

site-packages should be treated as read-only #117

milahu opened this issue Jan 16, 2024 · 2 comments

Comments

@milahu
Copy link

milahu commented Jan 16, 2024

this is just wrong...

const PACKAGE_PATH = join(__dirname, 'package.json')
const LOCK_PATH = join(__dirname, NODE_PM === 'npm' ? 'package-lock.json' : 'yarn.lock')
const MOD_PATH = join(__dirname, 'node_modules')

this will try to install NPM packages to /lib/python3.10/site-packages/javascript/js/
but site-packages should be treated as read-only

for example, on nixos this throws

Error: EACCES: permission denied, open '/nix/store/95x44fm60xp1mqa4s5jf7036cl4hgchh-javascript-1.1.2/lib/python3.10/site-packages/javascript/js/package.json'

fix

expose-cache-path.patch

--- a/src/javascript/js/deps.js
+++ b/src/javascript/js/deps.js
@@ -4,9 +4,10 @@
 const { pathToFileURL } = require('url')
 
 const NODE_PM = process.env.NODE_PM || 'npm'
-const PACKAGE_PATH = join(__dirname, 'package.json')
-const LOCK_PATH = join(__dirname, NODE_PM === 'npm' ? 'package-lock.json' : 'yarn.lock')
-const MOD_PATH = join(__dirname, 'node_modules')
+const CACHE_PATH = process.env.JS_PY_BRIDGE_CACHE || ((process.env.HOME || '') + '/.cache/js-py-bridge')
+const PACKAGE_PATH = join(CACHE_PATH, 'package.json')
+const LOCK_PATH = join(CACHE_PATH, NODE_PM === 'npm' ? 'package-lock.json' : 'yarn.lock')
+const MOD_PATH = join(CACHE_PATH, 'node_modules')
 const log = (...what) => console.log('\x1b[1m', ...what, '\x1b[0m')
 
 class PackageManager {
@@ -21,12 +22,14 @@
     try {
       this.installed = JSON.parse(fs.readFileSync(PACKAGE_PATH))
     } catch (e) {
+      fs.mkdirSync(CACHE_PATH, { recursive: true })
       fs.writeFileSync(PACKAGE_PATH, '{\n\t"name": "js-modules",\n\t"description": "This folder holds the installed JS deps",\n\t"dependencies": {}\n}')
       this.installed = JSON.parse(fs.readFileSync(PACKAGE_PATH))
     }
   }
 
   save () {
+    fs.mkdirSync(CACHE_PATH, { recursive: true })
     fs.writeFileSync(PACKAGE_PATH, JSON.stringify(this.installed, null, 2))
   }
 
@@ -75,9 +78,9 @@
         // work, since it will not actually save that into the Package Lock/JSON file. So we must first
         // put `latest` into the package.json, then run npm install to persist the `latest` version.
         this.setInstalledVersion(name, 'latest')
-        cp.execSync(`${NODE_PM} install`, { stdio: 'inherit', cwd: __dirname })
+        cp.execSync(`${NODE_PM} install`, { stdio: 'inherit', cwd: CACHE_PATH })
       } else {
-        cp.execSync(`${NODE_PM} install ${internalName}@npm:${name}@${version}`, { stdio: 'inherit', cwd: __dirname })
+        cp.execSync(`${NODE_PM} install ${internalName}@npm:${name}@${version}`, { stdio: 'inherit', cwd: CACHE_PATH })
       }
 
       process.stderr.write('\n\n')
--- a/src/javascript/connection.py
+++ b/src/javascript/connection.py
@@ -137,9 +137,18 @@ def readAll():
 def com_io():
     global proc, stdout_thread
     try:
+        env = dict(os.environ)
+        cache_env_name = "JS_PY_BRIDGE_CACHE"
+        if cache_env_name in os.environ:
+            node_path = os.environ[cache_env_name]
+            if node_path:
+                # append "/node_modules" to each path
+                node_path = node_path.replace(":", "/node_modules:") + "/node_modules"
+            env["NODE_PATH"] = node_path
         if os.name == 'nt' and 'idlelib.run' in sys.modules:
             proc = subprocess.Popen(
                 [NODE_BIN, dn + "/js/bridge.js"],
+                env=env,
                 stdin=subprocess.PIPE,
                 stdout=stdout,
                 stderr=subprocess.PIPE,
@@ -148,6 +157,7 @@ def com_io():
         else:
             proc = subprocess.Popen(
                 [NODE_BIN, dn + "/js/bridge.js"],
+                env=env,
                 stdin=subprocess.PIPE,
                 stdout=stdout,
                 stderr=subprocess.PIPE

now the cache path can be set with

import os
os.environ["JS_PY_BRIDGE_CACHE"] = "/path/to/cache/js-py-bridge"

from javascript import require

some_package = require("some-package")

with

$ ls /path/to/cache/js-py-bridge
node_modules
package.json
package-lock.json

JS_PY_BRIDGE_CACHE is passed to node as NODE_PATH
so this can be multiple paths separated by :

example use

@extremeheat
Copy link
Owner

extremeheat commented Jan 16, 2024

Hello, one solution to this is you pre-install the required dependencies or packages over CLI with

python -m javascript --install <package name>

In your setup.py or in another post-install step, as this would avoid any changes at runtime (you would still need to have write permissions initially). Perhaps this should be noted more explicitly in the docs that this avoids changing the package's state.

Otherwise if you need a way to change the module path, feel free to open a PR.

@milahu
Copy link
Author

milahu commented Jan 16, 2024

python -m javascript --install <package name>

not helpful on nixos, becaues __dirname always evals to
/nix/store/95x44fm60xp1mqa4s5jf7036cl4hgchh-javascript-1.1.2/lib/python3.10/site-packages/javascript/js
which is always read-only for packages using JSPyBridge

generally, storing state in site-packages is just wrong
like linux apps dont store state in /usr/lib

a way to change the module path

yepp, this is basic stuff for any package manager

feel free to use my expose-cache-path.patch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants