Copilot.el Without NodeJS
The GitHub Copilot language server releases include native language server binaries, meaning that you do not actually need NodeJS or NPM installed to use it.
The setup is quite straightforward to do manually:
- Get the URL to the latest version of the language server NPM package tarball:
curl -s https://registry.npmjs.org/@github/copilot-language-server/ | jq -r '.versions[."dist-tags".latest].dist.tarball' - Download and extract it to
copilot-install-dir, in the directory structure that Copilot.el expects (noting the manually created symlink inbin):copilot-install-dir ├── bin │ └── copilot-language-server -> ../lib/node_modules/@github/copilot-language-server/native/linux-x64/copilot-language-server └── lib └── node_modules └── @github └── copilot-language-server ├── <contents of the tarball's `package' directory go here> └── ...
I imagine that the intersection of developers who use Copilot and developers that do not want to install NodeJS is quite small, but I thought it would be worth mentioning in case it others find it useful, or if it is something that could be supported in Copilot.el (even if it is just some documentation in README).
I had to install a newer version of NodeJS than my OS package manager provided out of the box (Ubuntu 22.04), so getting NodeJS automatically when installing Emacs Copilot would have been convenient for me. That said, it was easy to install the newer version of NodeJS from a PPA.
I think the easiest way to install Node.js is by using something like NVM or mise.
The binaries included in the @github/copilot-language-server package make it so you don't have to install NodeJS at all.
I've also played around with containerising the Copilot language server with some success (noting that it needs file system access to the files where it is providing completion).
I imagine that the intersection of developers who use Copilot and developers that do not want to install NodeJS is quite small, but I thought it would be worth mentioning in case it others find it useful, or if it is something that could be supported in Copilot.el (even if it is just some documentation in README).
I'm fine with adding this in the README. PR welcome!
I imagine that the intersection of developers who use Copilot and developers that do not want to install NodeJS is quite small
installing npm on windows seems non-trivial (at least for me - msys does not include it and I don't use JS at all)
so, basically, any quant/DS on windows who wants to use copilot with python/scipy/sklearn is in this bucket.
this works for me:
(custom-set-variables
'(copilot-server-executable (expand-file-name "bin/copilot-language-server.exe"
copilot-install-dir)))
(defun my-copilot-install-server ()
"Install copilot language server."
(interactive)
(let* ((lib (concat copilot-install-dir "/lib/node_modules/"
(file-name-directory copilot-server-package-name)))
(cls (expand-file-name (file-name-nondirectory copilot-server-package-name)
lib))
(bin (expand-file-name "bin/" copilot-install-dir))
(src
(with-temp-buffer
(call-process-shell-command
(concat "curl -s https://registry.npmjs.org/"
copilot-server-package-name
"/ | jq -r '.versions[.\"dist-tags\".latest].dist.tarball'")
nil t)
(string-trim-right (buffer-string))))
(dst (expand-file-name (file-name-nondirectory src)
copilot-install-dir)))
(make-directory lib t)
(make-directory bin t)
(if (file-exists-p dst)
(message "already have [%s]" dst)
(message "getting [%s]" src)
(url-copy-file src dst)
(let* ((default-directory lib) buf
(status
(with-temp-buffer
(setq buf (current-buffer))
(call-process "tar" nil t nil "xfvz"
(concat "../../../" (file-name-nondirectory src))))))
(if (zerop status)
(kill-buffer buf)
(display-buffer buf)
(error "tar status=%s" status)))
(when (file-exists-p cls)
(let ((backup (concat cls "-bak")))
(message "backup [%s]" backup)
(delete-directory backup t)
(rename-file cls backup)))
(rename-file (expand-file-name "package" lib) cls)
(copy-file (expand-file-name "native/win32-x64/copilot-language-server.exe" cls)
bin t t))))
@nlordell Do you have any more you'd like to share about your containerized copilot-language-server? I'm trying to do the same - get a containerized emacs with copilot.el to connect to a copilot-language-server on a separate (node-based) container, and I've gotten close a few times with tricks like socat or netcat but it's not quite happy - the remote server seems to keep dying...
[Edit]: I just realized I should just be able to set the copilot-server-executable to a docker exec incantation, to start one on the node container. That should work, will try first thing tomorrow...
Do you have any more you'd like to share about your containerized copilot-language-server?
Sure! Its actually quite easy to setup, there are just a few gotchas to get working with copilot.el and github-language-server requirements.
Here is the Dockerfile for the copilot image I build:
FROM docker.io/library/debian:bookworm
ADD package/native/linux-x64/copilot-language-server /usr/local/bin/copilot-language-server
ENTRYPOINT ["/usr/local/bin/copilot-language-server"]
CMD ["--stdio"]
It assumes that you have downloaded and extracted the @github/copilot-language-server package before building the OCI image.
Then, you need to build the $EMACS_HOME/.cache/copilot/ directory for copilot.el. In particular, it needs a binary to launch (copilot-language-server - see below) and the package.json from the NPM package you should already have downloaded and extracted above in order to build the image. Here is the bear minimum directory structure you need:
$ tree .config/emacs/.cache/copilot
.config/emacs/.cache/copilot
├── bin
│ └── copilot-language-server
└── lib
└── node_modules
└── @github
└── copilot-language-server
└── package.json
The copilot-language-server needs to run a container for the image we created above. Assuming an image tag name of copilot, the contents of the shell script are:
#!/usr/bin/env bash
2>/dev/null \
exec podman run --rm \
-a stdout -a stdin -i \
-v "$HOME/.config/github-copilot":/root/.config/github-copilot \
-v "$HOME/code":"$HOME/code" \
--security-opt label=disable --pid host \
copilot \
"$@"
Note that I use podman instead of docker, but they should be interchangeable. Some of the gotchas:
- You need to attach
stdinandstdout, and run the container as--interactiveso thatcopilot.elcan communicate overstdinandstdoutwith the process. - I mount
$HOME/.config/github-copilotso that the login sessions persist across runs. - I mount
$HOME/codewhich is the root directory for all my code,github-copilot-serverneeds access to the files that it is providing completions for. This also means that if I try to usegithub-copilot-serverfor files that are outside of that mount point, then it will fail (which is fine for my case, since all my code lives in that subdirectory, and I only enablecopilot-modefor files in$HOME/code). -
--security-opt label=disableto disable some SELinux stuff. I've been meaning to setup proper SELinux policies for this, but have been lazy about it 😅. Without this, the container cannot access the mounted$HOME/.config/github-copilotand$HOME/codedirectories. If you don't use SELinux, you can omit this flag. -
--pid hostis required, as thegithub-copilot-serverneeds to see the process ID of the parent process. AFAIU, it uses it to setup "quit when my parent quits" logic and gets very mad when it cannot see the parent's PID.
I've tested this pretty extensively locally, and haven't run into any issues: it has the official Runs On My Machine™ stamp of approval 😛. Hope this helps!
I'm trying to do the same - get a containerized emacs with copilot.el to connect to a copilot-language-server on a separate (node-based) container
In my setup, my emacs runs in a toolbox container, and runs the github-copilot-server in a separate container. However, there are mechanisms to spawn additional Podman containers from within toolbox (specifically, flatpak-spawn --host podman allows you to execute podman commands on the host outside of the container, you can use this to run additional containers such as the github-copilot-server one above).
PS: I've moved from NeoVim to Emacs only a few months ago, and why I can't express these instructions in some Emacs Lisp code, forgive the blasphemy 😅.
@nlordell Thanks for all the details. I'm trying to set up a long-running container which does a few (node-related) things, so it doesn't run copilot-language-server as its main foreground process.
The idea is that a copilot.el client can substitute a wrapper script for copilot-language-server, which wrapper script does a
docker exec -it node-container copilot-language-server --stdio
The above starts out appearing to work. It says "Server Started Successfully" then "Already logged in as ..." (because I mounted the ~/.config/github-copilot/ as you mentioned. But then, the connection gets lost as we get "Server died" or similar. It's not keeping a sufficient stdio handle or something, when called through docker exec.
What is the -a stdin etc you have for your podman run command? I don't think we have that for docker exec.
I'm trying to set up a long-running container
Interesting, I typically just run the copilot-language-server as the sole process in the container (and start a new container in my "wrapper script"). I think this should work in theory.
What is the -a stdin etc you have for your podman run command?
This is just to attach the container main process's stdin and stdout when docker run-ing, AFAIU, docker exec attaches stdin and stdout (as well as stderr) by default.
docker exec -it [...]
I don't think you need the -t to allocate a pseudo-TTY, -i should be enough.
But then, the connection gets lost as we get "Server died" or similar.
Hmm, I don't know why that would happen... Out of curiosity, can you share how you are creating the container in the first place?
Hmm, I don't know why that would happen... Out of curiosity, can you share how you are creating the container in the first place?
Hi @nlordell, this may be more than you asked for, but you can start by cloning the devo branch of github.com/gornskew/skewed-emacs. It has a docker-compose config which will get 3 containers up and running: skewed-emacs, lisply-mcp, and gendl. All three containers wil get your ~/projects/ from the host mounted as /projects.
There will be an emacs server launched at startup in skewed-emacs, and lisply-mcp is a node-based container with copilot-language-server installed (but not launched at startup).
The repo for lisply-mcp is github.com/gornskew/lisply-mcp so you can look at its docker/Dockerfile and docker/build to see how the container is built (although you don't technically need that repo in order to run the container trifecta as follows):
git clone https://github.com/gornskew/skewed-emacs
cd skewed-emacs
git checkout devo
git pull
./compose-dev up
Then you can do:
docker exec -it --detach-keys "ctrl-@" skewed-emacs emacsclient -t
and that should give you an interactive terminal-mode emacs client running in skewed-emacs as user emacs-user (the --detach-keys avoids docker exec overriding your C-p for its default C-p C-q detach sequence).
From that emacsclient terminal, you can try M-x copilot-login and see what happens.
If you already have ~/.config/github-copilot in your host home directory, that will end up mounted into the lisply-mcp (node-based) container by the docker-compose, so it should already have you logged in when you try M-x copilot-login. I am able to get that far, but then I get "server died" or something like that. Here is the *Backtrace* I'm looking at:
Debugger entered--Lisp error: (jsonrpc-error "request id=1 failed:" (jsonrpc-error-code . -1) (jsonrpc-error-message . "Server died") (jsonrpc-error-data))
signal(jsonrpc-error ("request id=1 failed:" (jsonrpc-error-code . -1) (jsonrpc-error-message . "Server died") (jsonrpc-error-data)))
jsonrpc-request(#<jsonrpc-process-connection jsonrpc-process-connection-1e4b62107787> initialize (:processId 11 :capabilities (:workspace (:workspaceFolders t)) :initializationOptions (:editorInfo (:name "Emacs" :version "30.1") :editorPluginInfo (:name "copilot.el" :version "0.3.0-snapshot"))))
copilot--start-server()
copilot-login()
funcall-interactively(copilot-login)
command-execute(copilot-login record)
execute-extended-command(nil "copilot-login" "copilot-login")
funcall-interactively(execute-extended-command nil "copilot-login" "copilot-login")
command-execute(execute-extended-command)
You can see the Dockerfile in skewed-emacs/docker/ and lisply-mcp/docker/ respectively if you're curious how these are built.
The copilot.el customizations (which overrides the copilot-server-executable) are in skewed-emacs/dot-files/emacs.d/etc/copilot.el (the skewed-emacs codebase is also available as ~/skewed-emacs/ in the skewed-emacs container and its dot-files/emacs.d shows up symbolically linked as ~/.emacs.d)