copilot.el icon indicating copy to clipboard operation
copilot.el copied to clipboard

Copilot.el Without NodeJS

Open nlordell opened this issue 9 months ago • 11 comments

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:

  1. 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'
    
  2. Download and extract it to copilot-install-dir, in the directory structure that Copilot.el expects (noting the manually created symlink in bin):
    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).

nlordell avatar May 11 '25 18:05 nlordell

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.

ntc2 avatar May 26 '25 02:05 ntc2

I think the easiest way to install Node.js is by using something like NVM or mise.

bbatsov avatar May 26 '25 04:05 bbatsov

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).

nlordell avatar May 26 '25 06:05 nlordell

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!

bbatsov avatar May 26 '25 06:05 bbatsov

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.

sam-s avatar Jun 04 '25 14:06 sam-s

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))))

sam-s avatar Jun 04 '25 18:06 sam-s

@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...

genworks avatar Jun 27 '25 03:06 genworks

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 stdin and stdout, and run the container as --interactive so that copilot.el can communicate over stdin and stdout with the process.
  • I mount $HOME/.config/github-copilot so that the login sessions persist across runs.
  • I mount $HOME/code which is the root directory for all my code, github-copilot-server needs access to the files that it is providing completions for. This also means that if I try to use github-copilot-server for 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 enable copilot-mode for files in $HOME/code).
  • --security-opt label=disable to 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-copilot and $HOME/code directories. If you don't use SELinux, you can omit this flag.
  • --pid host is required, as the github-copilot-server needs 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 avatar Jun 27 '25 05:06 nlordell

@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.

genworks avatar Jun 28 '25 01:06 genworks

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?

nlordell avatar Jun 28 '25 06:06 nlordell

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)

genworks avatar Jun 28 '25 17:06 genworks