Skip to content

Conversation

mikeland73
Copy link
Contributor

@mikeland73 mikeland73 commented May 5, 2023

Summary

This adds the ability to include plugins explicitly via:

 "include": [
    "plugin:php"
  ]

in devbox.json. The idea is to allow including any devbox.json into an existing one. The proposed syntax is:

  • path: for local files
  • https:// for remote files
  • plugin: for built-in plugins

only plugin is implemented in this PR.

Improved existing plugin mechanism by using lockfile to decide is devbox.d directory needs to be created by keeping track of the plugin version. In the future we could allow upgrading of configs if there is version mismatch.

A few design questions: (cc: @Lagoja @loreto )

  • Should include be merged into packges instead? We could introspect the include to see if it's a flake or a plugin. This would simplify the devbox.json but it might be a weird way to include other configs.
  • One kinda ugly thing of current design is that the php plugin doesn't actually install PHP. That feels a bit wrong, but the way we've currently designed plugins is that they trigger on packages so not sure how to improve this. One option is to rename builtin to something that makes it more explicit that it extends functionality on existing nix packages. Maybe extend:php. Another option: In the future I expect includes to contain packages which would be installed as well. So could parametrize the php plugin so it can take the top level php package as an input (but otherwise it could install its own)

Edit:

After chatting with @loreto and @Lagoja we agreed to keep this design with following:

  • replace "builtin" with "plugin".
  • Including plugins works the same way current plugins. Specifically init hooks, and environment get added to existing environment. No new packages are installed.

How was it tested?

In php flake example, did devbox services up

@mikeland73 mikeland73 changed the title Landau/includes [WIP] config includes May 5, 2023
@mikeland73 mikeland73 requested review from savil and ipince May 8, 2023 23:50
@mikeland73 mikeland73 marked this pull request as ready for review May 9, 2023 00:07
@mikeland73 mikeland73 changed the title [WIP] config includes Config includes May 9, 2023
@loreto
Copy link
Contributor

loreto commented May 9, 2023

I like the functionality, but I'm unsure of the interface/schema in devbox.json (mainly due to similar concerns to the product questions you are raising). Worth chatting in person to flesh the interface out?

@Lagoja
Copy link
Contributor

Lagoja commented May 9, 2023

Should include be merged into packges instead? We could introspect the include to see if it's a flake or a plugin. This would simplify the devbox.json but it might be a weird way to include other configs.

I lean towards not including this in packages for now. While it might simplify the config, plugins and other devbox.json configs feel like a separate thing from packages (which ultimately tie back to a Nix package, either from a Flake or the nixpkgs repo). Having it in includes is a bit clearer.

One kinda ugly thing of current design is that the php plugin doesn't actually install PHP. That feels a bit wrong, but the way we've currently designed plugins is that they trigger on packages so not sure how to improve this. One option is to rename builtin to something that makes it more explicit that it extends functionality on existing nix packages. Maybe extend:php. Another option: In the future I expect includes to contain packages which would be installed as well. So could parametrize the php plugin so it can take the top level php package as an input (but otherwise it could install its own)

I think we might want to rename the plugin from php to something else? Maybe being explicit and calling it builtin:php-plugin or builtin:php-config might make this clearer. Based on the answer above, we should make sure these are distinct from packages

Happy to chat about this in person to flesh out.

@savil
Copy link
Collaborator

savil commented May 9, 2023

suggestion: avoid calling it builtin, because that usually implies a concept that ships with devbox. For example, in programming languages builtin functions ship with the language runtime. In our case, we do envision a world where plugins could be written by users, and those would not be "builtin".

Alternatives: "plugin: php", or "extension: php-plugin".

@mikeland73
Copy link
Contributor Author

suggestion: avoid calling it builtin, because that usually implies a concept that ships with devbox

Yep, same page. This is precisely why I called them builtin. builtin can only be used with plugins in our plugin directory which are compiled into devbox. For any other plugins, they would be referred to as path or https URLs that would point to a relative local path or a remote address.

@mikeland73 mikeland73 changed the title Config includes [devbox.json] Allowing including plugins explicitly via "include" field. May 10, 2023
Copy link
Collaborator

@savil savil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one concern with the implementation on plugin/plugin.go:245. I think it may be backwards incompatible: it may lead to undesirable overwriting of files for existing devbox-projects. WDYT?


How does a user discover which plugins are available?

  • (short term) Could we have a docs page that we reference in the help notes?
  • (long term) implement devbox plugins ls?

Including plugins works the same way current plugins. Specifically init hooks, and environment get added to existing environment. No new packages are installed.

what is the behavior a user would experience if they have includes: [ plugins: php ] but no package php?

// path: for local files
// https:// for remote files
// plugin: for built-in plugins
// This is similar to nix inputs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind elaborating on what you mean by "similar to nix inputs": in what way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nix flake inputs are urls of this format (path, https, github, etc) so we are using the same format.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, got it. Perhaps "This format is similar to nix inputs"

return err
}

d.pluginManager.ApplyOptions(plugin.WithAddMode())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when does this get applied now, in the flow for adding a package?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I understand this one. The AddMode is replaced with Lockfile.

// Only create devboxDir files in add mode.
if strings.Contains(filePath, devboxDirName) && !m.addMode {
func (m *Manager) shouldCreateFile(pkg *lock.Package, filePath string) bool {
// Only create files in devboxDir is they are not in the lockfile
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: if

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 for older plugins (i.e. not in the lockfile), this code is re-creating their devbox.d files.

  • I thought users were encouraged to commit them to source-control and possibly change them.
  • so, shouldn't we avoid re-generating these files for existing devbox projects

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make sure we don't overwrite them, but yeah this will cause a one time re-creation if they previously deleted/moved them. I think that's on ok tradeoff in order to migrate to a better system.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it

Yeah, I was concerned about the overwriting scenario where users may have made changes.

if err := d.lockfile.Add(includes); err != nil {
return err
}
if err := d.pluginManager.Include(d.writer, includes, d.projectDir); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be good for some future PR to make the plugin manager life-cycle more clear.

Suggestion:

  • pluginManager.New(lockfile) function that does applyOptions
  • pluginManager.Generate(packages, includes)
    • this can do lockfile.Add(includes) internally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewManager already applies options. The reason we call it separately is because of a circular dependency (see impl.Open function)

Agree with unifying Create and Includes

Not sure about calling lockfile.Add(includes) internally. That feels like logic that is unrelated to plugins. Insterad, plugins should be saved to lock file and then we can pass in resolved packages directly to plugin manager, which will mean we no longer need the lockfile in there.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewManager already applies options. The reason we call it separately is because of a circular dependency (see impl.Open function)

Oh I see. I left a comment above about improving that so its easier to reason about.

Not sure about calling lockfile.Add(includes) internally. That feels like logic that is unrelated to plugins. Insterad, plugins should be saved to lock file and then we can pass in resolved packages directly to plugin manager, which will mean we no longer need the lockfile in there.

Yup, you're right. Lets not call lockfile.Add from within pluginManager. 👍🏾

}
}

for _, includes := range d.cfg.Include {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: shouldn't this be singular include?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, in addition, I'm being thrown off by d.cfg.Include being singular....should that be plural?

Relatedly, in the devbox.json, should the field be includes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"include" is a verb, not a noun so I think it should be singular (e.g. tsconfig.json also uses "include" and not "includes"). @Lagoja curious if you have opinion.

For the range variable, I'll change it, maybe to included?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh its a verb! I see i see.
Okay, we can leave as is for devbox.json, and yeah I like included for the range var 👍🏾

// path: for local files
// https:// for remote files
// plugin: for built-in plugins
// This is similar to nix inputs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, got it. Perhaps "This format is similar to nix inputs"

}
}

for _, includes := range d.cfg.Include {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, in addition, I'm being thrown off by d.cfg.Include being singular....should that be plural?

Relatedly, in the devbox.json, should the field be includes?

if err != nil {
return nil, err
}
box.pluginManager.ApplyOptions(plugin.WithLockfile(lock))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest doing:

  1. remove pluginmanager: plugin.NewManager(), above
  2. make this box.pluginManager = plugin.NewManager(plugin.WithLockfile(lock))
  3. make applyOptions an internal function,

(3) would make NewManager a more genuine constructor. That would make it easier to reason about that all the pluginManager's fields are ready and methods can be applied.

if err := d.lockfile.Add(includes); err != nil {
return err
}
if err := d.pluginManager.Include(d.writer, includes, d.projectDir); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewManager already applies options. The reason we call it separately is because of a circular dependency (see impl.Open function)

Oh I see. I left a comment above about improving that so its easier to reason about.

Not sure about calling lockfile.Add(includes) internally. That feels like logic that is unrelated to plugins. Insterad, plugins should be saved to lock file and then we can pass in resolved packages directly to plugin manager, which will mean we no longer need the lockfile in there.

Yup, you're right. Lets not call lockfile.Add from within pluginManager. 👍🏾

// Only create devboxDir files in add mode.
if strings.Contains(filePath, devboxDirName) && !m.addMode {
func (m *Manager) shouldCreateFile(pkg *lock.Package, filePath string) bool {
// Only create files in devboxDir is they are not in the lockfile
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it

Yeah, I was concerned about the overwriting scenario where users may have made changes.

@mikeland73
Copy link
Contributor Author

How does a user discover which plugins are available?

(short term) Could we have a docs page that we reference in the help notes?

Built in plugin functionality is already documented. Plugins are not new, they were just only included by package whereas this allows them to be included explicitly.

(long term) implement devbox plugins ls?

Maybe. There's also a world where plugins are as ubiquitous as packages, and most of them are third party. So a search might be more appropriate than an ls.

what is the behavior a user would experience if they have includes: [ plugins: php ] but no package php?

The service will fail. For other plugins the init hooks will fail. I think this is OK. The match mechanism in built in plugins is a bit too magical, and I'm not sure we should expand it to check if you have correct packages installed. Not sure.

@mikeland73
Copy link
Contributor Author

@savil I confirmed that files in devbox.d do not get replaced if they already exists (This is existing functionality)

@mikeland73 mikeland73 merged commit e2478a2 into main May 15, 2023
@mikeland73 mikeland73 deleted the landau/includes branch May 15, 2023 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants