One can use use-package
. In a file (which could be your init file, but doesn't have to be):
(require 'package)
(add-to-list 'package-archives
'("melpa" . "https://melpa.org/packages/") t)
(package-initialize)
(setq package-archives
(seq-remove (lambda (element)
(equal "gnu" (car element)))
package-archives))
(add-to-list 'package-archives
'("gnu" . "https://elpa.gnu.org/packages/"))
;; bootstrap use-package, so I can use it to manage everything else
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
(eval-when-compile (require 'use-package))
(use-package use-package
:config
(setq use-package-always-ensure t))
Then you can set up all your packages as follows:
(use-package package1)
(use-package package2)
When this file is evaluated (which, if it's your init file, is on startup), Emacs will install all the packages you've set up this way. If a package is already installed, use-package will skip the installation; it won't be updated to the latest. You can use other features of use-package to configure each installed package.