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

Create a rules_sass BUILD file generator #75

Closed
wants to merge 6 commits into from

Conversation

achew22
Copy link
Member

@achew22 achew22 commented Feb 18, 2019

This change uses Gazelle (https://github.com/bazelbuild/bazel-gazelle),
a BUILD file generator for Bazel projects, to generate rules_sass BUILD
files automatically based on the directory structure of a preexisting
project. This supersedes #66.

Gazelle intelligently manages your BUILD files. It doesn't delete any
rules (unless it is appropriate to delete it), operates off of a cache,
and is fast enough to be used by the Kubernetes project for managing
their BUILD files.

How to use this:

  1. Follow the instructions in the Gazelle documentation
    for configuring Gazelle.
  2. In the BUILD file in the root of your project, amend the
    list of installed languages to additionally include the target,
    which is created in this commit,
    "@io_bazel_rules_sass//gazelle:go_default_library",
  3. cd into the directory you would like to generate BUILD files for
  4. bazel run //:gazelle
  5. Enjoy your new BUILD files.

Copy link
Collaborator

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

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

High level: would it make more sense to move the code for all of the gazelle related stuff to its own repository? The addition dwarfs the existing code and I don't believe it needs to be tightly coupled in the same repo.

BUILD.bazel Show resolved Hide resolved
)

# In your project you should define your own Gazelle binary. Follow the
# instructions at
Copy link
Collaborator

Choose a reason for hiding this comment

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

@alexeagle we generally avoid people having to do things like this in the Angular ecosystem, no?

Copy link
Contributor

Choose a reason for hiding this comment

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

we don't want our users to compile Go code, no. But we haven't tackled the details of how that works along with Gazelle build rules yet.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure how to proceed on this.

Choose a reason for hiding this comment

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

I believe users could run bazel run @io_bazel_rules_sass//:gazelle without needing to define their own rule. I'm guessing something in the Angular CLI would do that though?

Users will still need to ensure @bazel_gazelle and its dependencies are declared in WORKSPACE.

Let me know if there would be useful changes in the gazelle or gazelle_binary rules to make this easier.

Copy link
Contributor

Choose a reason for hiding this comment

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

For the whole npm ecosystem we don't want our users to observe a dependency on go code since they have to download a whole Go sdk. Our approach for other go code has been to ship three binaries to npm. I imagine we'll eventually do the same for BUILD file generation - so you'd get something like bazel run @npm//@bazel/gazelle/bin:gazelle and it would not require any dependencies in WORKSPACE

@@ -4,8 +4,8 @@ load("//sass:sass.bzl", "sass_binary")

# Import our shared colors and fonts so we can generate a CSS file.
sass_binary(
name = "nested",
src = "dir/main.scss",
name = "dir",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why change this name?

Copy link
Member Author

Choose a reason for hiding this comment

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

I ran gazelle over these directories. If you would like them restored to their former glory because you were using them as tests/examples of functionality, just say so.

name = "nested",
src = "dir/main.scss",
name = "dir",
src = "main.scss",
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the point of this test was was to reference a file one directory deep

)

# make a :shared target that includes both :colors and :fonts.
sass_library(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why delete this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Gazelle doesn't make a :shared target so I deleted it by hand. Note that if I hadn't deleted it, gazelle would have left it alone and it would still be there. Gazelle only updates rules that it would have created if the directory were empty.

gazelle/config.go Outdated Show resolved Hide resolved
gazelle/BUILD.bazel Outdated Show resolved Hide resolved
gazelle/fileinfo.go Outdated Show resolved Hide resolved
gazelle/fileinfo_test.go Outdated Show resolved Hide resolved
gazelle/parser/ident.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@nex3 nex3 left a comment

Choose a reason for hiding this comment

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

I don't understand why this scans numbers, functions, hashes, URLs, or any sigils other than @ and ,. All you care about is finding the string @import outside of a comment, and reading strings after it.

gazelle/BUILD.bazel Outdated Show resolved Hide resolved
gazelle/fileinfo.go Outdated Show resolved Hide resolved
gazelle/fileinfo.go Outdated Show resolved Hide resolved
gazelle/fileinfo.go Outdated Show resolved Hide resolved
gazelle/fileinfo.go Outdated Show resolved Hide resolved
gazelle/parser/comment.go Outdated Show resolved Hide resolved
gazelle/parser/string.go Outdated Show resolved Hide resolved
@achew22
Copy link
Member Author

achew22 commented Feb 24, 2019

High level: would it make more sense to move the code for all of the gazelle related stuff to its own repository? The addition dwarfs the existing code and I don't believe it needs to be tightly coupled in the same repo.

It's totally possible to break this out into its own repo. I believe it should be included in this repo. As I wrote in #66,

Why should this be in the rules_sass repo?

Great question! I believe this should live in the rules_sass repo so
that the generator of BUILD files and the macros/rules in the repo can
be iterated on in lock step. This will help prevent circumstances where
a user of rules_sass forgets to update their dependency on Gazelle and
ends up generating garbage BUILD files that are only logically to a
former state of the rules_sass project.

To expand on this, Gazelle is the best story that Bazel has for iterating on rules_* repos. By using your source files as a declarative language for creating BUILD files, you can instruct users to invoke gazelle fix and perform any manipulation to the AST of the BUILD file that you need. If you are going to add a parameter to your BUILD files why not have gazelle fix it for your users when they next run gazelle after updating the SHA to load your repo?

@achew22 achew22 force-pushed the tokenizer branch 2 times, most recently from dd4d8b1 to cffc2c2 Compare February 24, 2019 23:41
@jelbourn
Copy link
Collaborator

I talked to @alexeagle and he agreed that ti make sense for the code to be here, so sgtm.

Looks like the CI is failing on the Windows build.

@nex3
Copy link
Collaborator

nex3 commented Feb 26, 2019

I think this comment is still unaddressed...

Copy link

@jayconrod jayconrod left a comment

Choose a reason for hiding this comment

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

I'm excited to see this making progress. A few comments on rule generation, parser structure, and Go readability. Structure generally looks good though. I'm afraid I don't know enough about sass to comment on anything related to the syntax though.

)

# In your project you should define your own Gazelle binary. Follow the
# instructions at

Choose a reason for hiding this comment

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

I believe users could run bazel run @io_bazel_rules_sass//:gazelle without needing to define their own rule. I'm guessing something in the Angular CLI would do that though?

Users will still need to ensure @bazel_gazelle and its dependencies are declared in WORKSPACE.

Let me know if there would be useful changes in the gazelle or gazelle_binary rules to make this easier.

limitations under the License.
*/

package gazelle

Choose a reason for hiding this comment

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

This is kind of a nit, but consider renaming this to package sass or package sasslang.

For the most part, this would only be imported in generated code, so the package name doesn't matter too much, but in some cases, language extensions do refer to definitions in other extensions. For example, the Go extension refers to information extracted by the proto extension (which is package proto) in order to generate go_proto_library rules.

If you decide to change this, it would probably also mean moving this package to a subdirectory //gazelle/sass so that the package name matches the end of the import path.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done


file, err := os.Open(filepath.Join(dir, name))
if err != nil {
log.Printf("%s: error reading sass file: %v", info.Path, err)

Choose a reason for hiding this comment

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

Consider returning an error and letting Generate handle the error (by printing it). sassFileInfo may not know what the caller wants to do in case of error.

It's okay to return a partial FileInfo in cases where an error has occurred, but it might be good to distinguish fatal errors (file couldn't be opened) from non-fatal errors (couldn't parse).

Copy link
Member Author

Choose a reason for hiding this comment

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

It's okay to return a partial FileInfo in cases where an error has occurred, but it might be good to distinguish fatal errors (file couldn't be opened) from non-fatal errors (couldn't parse).

Handled by returning an error in the err value in the fatal case (file not being able to be opened) and adding an "Errors" array to the FileInfo object to represent non-fatal parsing errors.

parseError := func(msg string, args ...interface{}) {
// Prepend the file.Name() so we tell the user which file had the parse error.
args = append([]interface{}{file.Name()}, args...)
fmt.Fprintf(os.Stderr, "%s: "+msg+"\n", args...)

Choose a reason for hiding this comment

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

If you end up returning an error, this function could write to a strings.Builder, then call errors.New with that at the end if it's non-empty.

Copy link
Member Author

Choose a reason for hiding this comment

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

Resolved with the Errors being first class in the FileInfo object


got := sassFileInfo(dir, tc.name)

// Reexpose the fields we care bout for testing.

Choose a reason for hiding this comment

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

Nit: maybe reword to "Clear the fields we don't care about for testing"

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

gazelle/sass.go Outdated
// Any non-fatal errors this function encounters should be logged using
// log.Print.
func (s *sasslang) GenerateRules(args language.GenerateArgs) language.GenerateResult {
files, err := ioutil.ReadDir(args.Dir)

Choose a reason for hiding this comment

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

You should be able to use args.Subdirs, args.RegularFiles, and args.GenFiles instead of reading the directory again.

Avoid panicking on I/O errors. Most errors in GenerateRules should simply be logged.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

gazelle/sass.go Outdated
// base is the last part of the path for this element. For example:
// "hello_world" => "hello_world"
// "foo/bar" => "bar"
base := filepath.Base(args.Rel)

Choose a reason for hiding this comment

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

Rel will be slash-separated, so use path.Base instead of filepath.Base.

Note that Rel will be "" for the root directory, and path.Base will return "." for that, so you'll probably want a special case.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

gazelle/sass.go Outdated
// Libraries in Sass have filenames that start with _.
base = filepath.Base(f)

rule := rule.NewRule("sass_library", base[1:len(base)-5])

Choose a reason for hiding this comment

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

So each _ file gets its own sass_library rules? You'll probably want a way to delete sass_library rules after those files have been removed. You can read rules in args.File and generate empty rules (returned through GenerateResult.Empty) if no rules are generated with those names. The empty rules will be merged, and if there's nothing left, the merged rules will be deleted.

Copy link
Member Author

Choose a reason for hiding this comment

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

Since I only expect there to be a single sass_library entry per directory by the name of the directory (if you're in //foo, the target would be //foo:foo), I've decided to add an empty rule.NewRule("sass_libary", base) to the Empty list in all cases. Since Gazelle does a resaonble thing and doesn't delete it if it was generated and deleted this seems like a safe decision. Can you please review this logic.

gazelle/sass.go Outdated
func (s *sasslang) Kinds() map[string]rule.KindInfo {
return map[string]rule.KindInfo{
"sass_library": {
MatchAny: true,

Choose a reason for hiding this comment

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

MatchAny means that when merging a sass_library, the merger can match any target with the kind sass_library, regardless of name. This probably doesn't make sense if there may be multiple sass_library targets in the same build file.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@@ -0,0 +1,7 @@
# gazelle:exclude .

Choose a reason for hiding this comment

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

An exclude directive can prevent Gazelle from recursing into a subdirectory, but a directory can't exclude itself. So this should be in the parent.

Copy link
Member Author

Choose a reason for hiding this comment

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

This has been resolved upstream, but I also fixed it in the PR.

@achew22
Copy link
Member Author

achew22 commented Mar 11, 2019

@nex3

I think this comment is still unaddressed...

To quote the actual comment (which I apparently can't reply directly to in Github?!?!)

I don't understand why this scans numbers, functions, hashes, URLs, or any sigils other than @ and ,. All you care about is finding the string @import outside of a comment, and reading strings after it.

Sorry for not responding to this earlier, I find the GitHub review interface a great reminder of how wonderful Critique + Gerrit are.

It scans numbers for a very simple reason, the CSS3 spec specifies how that they should be handled and it is known correct. When implementing a partial parser for SASS files, I was able to come up with examples where something fell through the cracks and didn't lex well. To correct for these, I just implemented that section of the CSS3 spec and any function that was called from inside of that function according to the spec. By the time I finished getting the parser to get all the testcases I could think of, I was almost all the way to a full parser, probably close to 90% and adding in the rest wasn't particularly difficult or very many lines of code.

@nex3
Copy link
Collaborator

nex3 commented Mar 11, 2019

Sorry for not responding to this earlier, I find the GitHub review interface a great reminder of how wonderful Critique + Gerrit are.

I agree strongly 😝.

It scans numbers for a very simple reason, the CSS3 spec specifies how that they should be handled and it is known correct. When implementing a partial parser for SASS files, I was able to come up with examples where something fell through the cracks and didn't lex well. To correct for these, I just implemented that section of the CSS3 spec and any function that was called from inside of that function according to the spec. By the time I finished getting the parser to get all the testcases I could think of, I was almost all the way to a full parser, probably close to 90% and adding in the rest wasn't particularly difficult or very many lines of code.

What cases are you thinking of? Numbers can never contain the character @, nor can they contain quotes or /, so they shouldn't ever be a problem for lexing when looking for @import. Same goes for the other tokens that I'm advocating you ignore.

I really think the best approach here (in terms of producing a small amount of code with high accuracy) is to just blindly consume characters until you hit /*, ", ', \, or @import. But if you're willing to maintain the extra code that comes from lexing every CSS production, I suppose that's all right.

@alexeagle
Copy link
Contributor

Hey there, what does this need to proceed? There's a team in Bazel that owns BUILD file generation now. Maybe they could poke at this?

@achew22
Copy link
Member Author

achew22 commented Jul 19, 2019

@alexeagle, I don't think there is anything holding this back other than Windows compatibility, for which I don't have a machine. I don't know exactly what's necessary here but I don't imagine it is much.

Once this is merged, would they be willing to work in it out of google3 or third party?

@nex3
Copy link
Collaborator

nex3 commented Aug 9, 2019

@alexeagle If that team would be willing to take this on, that would be awesome. Do you know if they mostly use Gazelle for their tooling, or something else? Looking through the Gazelle README, it seems to be saying that it's built just for Go projects, which confuses me.

This change uses Gazelle (https://github.com/bazelbuild/bazel-gazelle),
a BUILD file generator for Bazel projects, to generate rules_sass BUILD
files automatically based on the directory structure of a preexisting
project. Gazelle recently added support for plugins and I wanted to try
writing a new plugin for it.

Gazelle intelligently manages your BUILD files. It doesn't delete any
rules (unless it is appropriate to delete it), operates off of a cache,
and is fast enough to be used by the Kubernetes project for managing
their BUILD files.

Why should this be in the rules_sass repo?

Great question! I believe this should live in the rules_sass repo so
that the generator of BUILD files and the macros/rules in the repo can
be iterated on in lock step. This will help prevent circumstances where
a user of rules_sass forgets to update their dependency on Gazelle and
ends up generating garbage BUILD files that are only logically to a
former state of the rules_sass project.

How to use this:

1.  Follow the instructions on the Gazelle website for configuring
    Gazelle.
2.  In the `BUILD` file in the root of your directory, amend the
    list of installed languages to additionally include the target,
    which is created in this commit,
    "@io_bazel_rules_sass//gazelle:go_default_library",
3.  `cd` into the directory you would like to generate BUILD files for
4.  `bazel run //:gazelle`
5.  Enjoy your new BUILD files.
@alexeagle
Copy link
Contributor

It's Patrick Doyle and team (stopwatch team) who own build_cleaner, mostly concerned with Java. They aren't doing Bazel/external yet but it's on their roadmap to think about it. Gazelle seems like a likely mechanism since it already has some adoption.
Gazelle was built for Go and the multi-language support was added later. I imagine the documentation is lagging, or maybe there just isn't much happening with other languages yet.

@nex3
Copy link
Collaborator

nex3 commented Aug 14, 2019

Great, thanks! I've reached out to get his take on what the future of BUILD tooling plugins will look like.

@ashi009
Copy link

ashi009 commented Nov 29, 2019

9 months later, this is still not merged. :(

@mrmeku
Copy link

mrmeku commented Dec 5, 2019

@nex3 @alexeagle Can you two clarify what work is left to be done to get this merged? I have a windows machine and and a willingness to subject myself to windows development for a good cause

@nex3
Copy link
Collaborator

nex3 commented Dec 17, 2019

I'd like to see a dramatically simpler parser that only covers the necessary tokens, and all the tests need to pass.


rules = append(rules, rule)
} else {
normalFiles = append(normalFiles, f)
Copy link

Choose a reason for hiding this comment

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

Won't this make the imports array and the rules array not have the same length (or be associated by index?)
GenerateResult implies they need exactly the same number of items as Resolve will be called with each index in tandem?

(I'm using your PR to get a feel for how gazelle generation works, so I'm new to this and might not be following correctly)

Copy link
Member Author

Choose a reason for hiding this comment

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

If you're looking for a better example, you can check out bazelbuild/bazel-skylib#251

Copy link

Choose a reason for hiding this comment

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

Ok, seeing the setPrivateAttr(config.GazelleImportsKey) part makes me realize I have no idea why GenerateResult has Gen and Imports...

Sorry for the spam, like I said just wrapping my head around this.

Copy link

Choose a reason for hiding this comment

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

If you're looking for a better example, you can check out bazelbuild/bazel-skylib#251

ty, I'll take any example I can get 😄

@achew22 achew22 closed this Jul 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants