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

Support sass-embedded as alternative implementation #124

Merged
merged 33 commits into from
Oct 15, 2021
Merged

Support sass-embedded as alternative implementation #124

merged 33 commits into from
Oct 15, 2021

Conversation

ntkme
Copy link
Contributor

@ntkme ntkme commented Sep 16, 2021

This PR add support for dart based sass implementation.

I have enabled edit access for maintainers so please free feel to make changes.

New

  • New configuration key implementation that can be either sassc or sass-embedded. Default to sassc for compatibility.

Limitations

  • sass-embedded gem will not be added as a runtime dependency for now as dart-sass-embedded still beta. Meaning user who wish to use this will have to manually add sass-embedded to Gemfile.

@ashmaroli
Copy link
Member

@ntkme Thanks for opening this WIP PR.
sass-embedded requires a minimum Ruby version of 2.6.0. So before further developments, please update the requirement in the gemspec and CI config files.

@ashmaroli
Copy link
Member

@ntkme For future reference, you need not rebase unless there are merge conflicts, especially in a draft PR. We always squash all commits while merging a pull request.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 17, 2021

Appveyor with ruby 2.7 and 3.0 is having some issue that I don't quite understand.
Appveyor with ruby 2.6 is stuck installing sass-embedded. - This step is just downloading three artifacts from GitHub: protoc, embedded-protocol, dart-sass-embedded. Not sure how could that stuck for an hour. I think it is likely an environment setup issue, but I don't know how to debug this.

sass-embedded gem package has been tested on windows using GitHub actions. For example: https://github.com/ntkme/sass-embedded-host-ruby/actions/runs/1240509193

So, it should work in windows with the right setup, not sure what's wrong with Appveyor.

Shall we try debug Appveyor or maybe we can move windows tests to use GitHub actions?

@ashmaroli
Copy link
Member

So, it should work in windows with the right setup, not sure what's wrong with Appveyor.
Shall we try debug Appveyor or maybe we can move windows tests to use GitHub actions?

@ntkme I've pushed a commit to migrate tests for Windows to GH Actions (just for this branch). While the installation succeeds, the test suite fails anyways since the expected output is not the same for sourcemaps for Linux and Windows for the two implementations.
Hopefully, you can fix it or is that for dart-sass to handle?

In the meantime, I'll think of another way to run the specs for two implementations without indenting the existing spec (to improve maintainability, ease running a single failing spec and to reduce the overall diff on this pull request.)

@ntkme
Copy link
Contributor Author

ntkme commented Sep 19, 2021

The source map issue has to do with dart-sass converting D:/... to D/... for windows, and that breaks the native directory handling in ruby. It should to be trivial to fix and I will take a look.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 19, 2021

I fixed the the source map issue on windows. Tests now passes for ruby 2.7/3.0 on windows.

However, Ruby 2.6 on windows would get stuck during dependencies installation.

@phisch
Copy link

phisch commented Sep 19, 2021

@ntkme that looks like a classic "rerun and succeed" CI issue...

@ntkme
Copy link
Contributor Author

ntkme commented Sep 19, 2021

Attempted to run bundle install outside of setup-ruby action, and it worked. But bundle install inside setup-ruby with ruby 2.6 does not work, even with a new cache-version set. Not sure why.

@ashmaroli
Copy link
Member

@ntkme I don't know what the issue is with Ruby 2.6 and Windows.
Since this was evident on AppVeyor as well, I have removed sass-embedded as a runtime dependency. Users will have to manually include the gem in their dev environment either via Gemfile or direct install.

Currently, the loading is within the :convert method. It is less than ideal but adequate for the time being.

Now that the CI has passed (I removed Windows-Ruby2.6 from the matrix), let us focus on the documentation aspect.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 20, 2021

@ashmaroli Sure. Let's do that for now. I opened ruby/setup-ruby#218, hope we can get some help.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 21, 2021

@ashmaroli Manged to get access to a windows machine and fixed ruby 2.6 on windows. Test now passes. Details is here if anyone is interested: ruby/setup-ruby#218 (comment)

@ashmaroli
Copy link
Member

That is wonderful investigative work @ntkme. 🎉
Thanks for fixing things so quickly.

@ashmaroli
Copy link
Member

@ntkme I tested this on an old system running on Windows 7 SP1. Latest versions of Ruby and Jekyll installs and runs fine on that system but it doesn't have a newer version of Powershell. (Windows 7 ships with just PS 2.0; PS 5+ is needed for running Expand-Archive cmdlet).

Therefore, in the interest of Jekyll users still on Windows 7, I suggest not having sass-embedded as a runtime dependency of this plugin.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 21, 2021

It looks like rubyinstaller2 provides gunzip command from msys2. I can check if gunzip is available and fallback to Expand-Archive.

Never mind. Forgot that gunzip does not work with zip that contains more than one files.

I looked at how sass/embedded-host-node handles this problem and it runs out they have added a dependency just to handle zip files. So, we can do the same here.

@ashmaroli
Copy link
Member

ashmaroli commented Sep 21, 2021

@ntkme I have decided to not include sass-embedded as a runtime dependency of the upcoming release of this plugin.
Users willing to try dart-sass are welcome to make the switch willingly; so your work here will not go to waste.

Slowly, as the ecosystem stabilizes, we'll probably phase out sassc as the primary implementation and migrate to dart-sass out-of-the-box.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 21, 2021

@ashmaroli That's fine with me. Please feel free to push changes to do that.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 21, 2021

@ashmaroli By the way, excluding it from runtime dependencies means that we can have this feature released even with dart-sass-embedded being in beta, as ultimately it is end users' choice to manually install this unstable dependency. Is that what you meant by "upcoming release"?

@ashmaroli
Copy link
Member

have this feature released even with dart-sass-embedded being in beta..

Yes, that is the plan.
I only mentioned "upcoming" in the lax sense. There will surely be a minor-version bump and this feature will be a nice addition.
However, there is no fixed date, so no hurry. You can contribute in your free time.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 21, 2021

Windows 7 now works with some VBS magic to do the unzip work: sass-contrib/sass-embedded-host-ruby@73a66b2

Manually tested with Windows 7 VM from https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/ and rubyinstaller2 (2.6.8-1)

@ashmaroli
Copy link
Member

Windows 7 now works with some VBS magic

Whoa! That was a wrong move, @ntkme
Your project just lost a couple of security points.

Using gems with native extensions are always a security concern. In the current scope, the gem already downloads third-party archives from the internet without user intervention during the installation, installs a native executable (dart), (none of this is logged in the stdout) and now there is a custom non-Ruby script also bundled within the gem (irrespective of whether that script runs for 99% of user-base).

Agreed, that everything is open-source and verifiable.
Agreed, that each concern has workarounds:

  • Compile gem manually from source.
  • Download archives manually, validate them and point to them during installation.
  • Install PowerShell 5.1+ (if on Windows)

Nevertheless, the default behavior has a negative impact on trustworthiness.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 21, 2021

The bundled VBS script is very explicit that it can only be used to unzip, nothing else, and it is only used if user does not have powershell 5.0+. I don’t see any problem just because the common misconceptions of VBS being associated with malware. I think this is a fair use case since there is no other solution out of box.

Compile the dart code would require the whole dart tool chain. In addition, it will need git or unzip or otherwise bundle whole source code with the project that can introduce a whole lot more errors.

Ask user to install powershell 5.0 might be something fair, but the user experience is not as good as this.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 21, 2021

Regardless of using VBS or not, users have to decide what gems they trust. I mean, even if a malicious gem does not have any extension it can still do harm at runtime as soon as a user requires it.

If people are so paranoid about VBS I can definitely revert it and just call it a day saying Windows 7 is not supported, as it is already end of support. If it wasn’t pointed out here that it wouldn’t work with windows 7, I won’t even bother try supporting an end of support system.

@ashmaroli
Copy link
Member

If people are so paranoid about VBS I can definitely revert it and just call it a day saying Windows 7 is not supported

It wasn't about VBS but having a custom script silently run as part of the installation. IMO, reverting the support would be best.
Also, if you cut a new release, you need not push a commit here to update the Gemfile. The version used in the action runner will get updated automatically (until you release a v0.8).

Also, I apologize for wasting your time by mentioning Windows 7.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 21, 2021

It wasn't about VBS but having a custom script silently run as part of the installation.

Overall the problem is that native dependencies which are not native language binding libraries does not really work with native extension build system (mkmf.rb or node-gyp), as those building systems are designed only for building native language binding libraries. sass team made the choice to to use dart compiled native binary and stdio communication instead of native binding as there is no easy way to create a dart-to-other-language binding. For example, to implement a dart-compiled-native-code-to-ruby-binding, the whole Dart VM need to be embedded into the ruby native binding library, and then a remote call interface need to be written to handle serialization of method, parameters, and return values between Dart VM methods/types and ruby c-extension methods/types. - That is possible but the development cost is too expensive.

On the other hand, use a custom script to handle installation of a native dependency that is not a native language binding library is actually pretty common nowadays. For example, in nodejs world, the "postinstall" hook can be used to run any script to handle additional setup or configuration to provide out of box user experience, and many projects use this feature to run custom script. In the official nodejs equivalent project sass/embedded-host-node, you can see that they use a custom script to download files during installation: https://github.com/sass/embedded-host-node/blob/46d600d239698fab9f811c059469d89efaaf3355/package.json#L24

You do have a good point about "silently run", but that's a design of rubygems. By default, extconf would only print Building native extensions. This could take a while..., but nothing else unless gem install is run with --verbose option. It's rubygems's design choice to "silently run" by default, I cannot really do anything about it. If user run try to install it with --verbose, I did make sure that each non-ruby command executed is also printed to the console, so that users can be aware of what has happened. There is nothing intentionally hidden.

By design ruby's mkmf.rb uses Makefile, thus any commands available on the system can be used under that. Practically running a VBS is not really different from writing the same functionality in ruby code or other language. embedded-host-node has dependencies extract-zip and tar to handle compressed file in purely nodejs code. We could do the same for ruby by adding rubyzip. However, there is a clear benefit of VBS: it's tiny and concise. From a security audit perspective, it's 113KB of ruby code (only counting files in the lib folder) vs 0.5KB of VBS to be inspected.

Finally, I understand people have concerns because the custom script "download" artifacts from internet. sass-embedded only download from sass' and protobuf's official GitHub releases. For restricted environment that may have limit access for public internet, it does offer an alternative offline installation method where user has the full control of dependencies being used:

https://github.com/ntkme/sass-embedded-host-ruby/blob/f1b0e66dfefdeeb1a3578da19a963a9da47aee1a/ext/sass/extconf.rb#L11-L24

An alternative that does not involve downloading or unzipping would be building platform native gem for each platform and embedding the uncompressed platform native dependencies directly in the gem itself. However, in the end that's less transparent (need to verify a 8.7MB binary from non-official distribution channel), therefore make it even worse than downloading from official release channel.

@ntkme
Copy link
Contributor Author

ntkme commented Sep 22, 2021

@ashmaroli I did a rebase because merging conflict is getting crazy.

Also, I changed the way that test works so that now this PR does not indent the test cases, so the changes would be much easier to review. Now test script would run twice in the CI with the environment variable SASS_IMPLEMENTATION controls which implementation to be used in test. This environment variable is test only and won't affect users.

@ntkme ntkme marked this pull request as ready for review September 22, 2021 19:12
@ashmaroli
Copy link
Member

I did a rebase because merging conflict is getting crazy.

No issues, it was inevitable.

I changed the way that test works so that now this PR does not indent the test cases,

Glad that fog has been cleared.

test script would run twice in the CI with the environment variable SASS_IMPLEMENTATION controls which implementation to be used in test.

This is less than ideal, but an acceptable trade-off. I'll see if we can get the best of both methods (former and current) after this PR gets merged.
Now, onto the review phase (although documentation for this feature is still scant).

Gemfile Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
ntkme and others added 4 commits September 29, 2021 03:17
Co-authored-by: Ashwin Maroli <[email protected]>
Co-authored-by: Ashwin Maroli <[email protected]>
Co-authored-by: Ashwin Maroli <[email protected]>
Co-authored-by: Ashwin Maroli <[email protected]>
README.md Outdated Show resolved Hide resolved
Copy link
Member

@mattr- mattr- left a comment

Choose a reason for hiding this comment

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

:shipit:

@ntkme
Copy link
Contributor Author

ntkme commented Oct 14, 2021

@parkr PTAL when you get a chance. Thanks!

Copy link
Member

@parkr parkr left a comment

Choose a reason for hiding this comment

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

A few questions from me.

* **`style`**

Sets the style of the CSS-output.
Can be `nested`, `compact`, `compressed`, or `expanded`.
See the [SASS_REFERENCE](https://sass-lang.com/documentation/cli/dart-sass#style)
for details.

Defaults to `compact`.
Defaults to `compact` for `sassc`.
Defaults to `expanded` for `sass-embedded`.
Copy link
Member

Choose a reason for hiding this comment

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

Why would we have two different defaults for the implementation? That seems counter to what I'd expect.

Copy link
Contributor Author

@ntkme ntkme Oct 14, 2021

Choose a reason for hiding this comment

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

See https://sass-lang.com/documentation/js-api/interfaces/LegacySharedOptions#outputStyle

The output style of the compiled CSS. There are four possible output styles:

  • "expanded" (the default for Dart Sass) writes each selector and declaration on its own line.
  • "compressed" removes as many extra characters as possible, and writes the entire stylesheet on a single line.
  • "nested" (the default for Node Sass, not supported by Dart Sass) indents CSS rules to match the nesting of the Sass source.
  • "compact" (not supported by Dart Sass) puts each CSS rule on its own single line.

The reason is that dart-sass has dropped support for compact and nested, and we don't want to change the default for sassc.

Copy link
Member

Choose a reason for hiding this comment

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

Gotcha. What happens if I specify an unsupported output style? Do I see an error? What does that error look like?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://github.com/ntkme/sass-embedded-host-ruby/blob/8913a3f521db5c2485344701745bd2839a68a59f/lib/sass/embedded/compile_context.rb#L218

But in this PR we check for the input and fallback to the respective default value so user won’t see any error.

@@ -113,9 +114,15 @@ def sass_dir
jekyll_sass_configuration["sass_dir"]
end

def sass_implementation
implementation = jekyll_sass_configuration["implementation"]
ALLOWED_IMPLEMENTATIONS.include?(implementation) ? implementation : "sassc"
Copy link
Member

Choose a reason for hiding this comment

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

Should we send a warn if the implementation is not allowed so the user can fix their config?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is aligned with the style option that it fallbacks to default value without warning. Shall we add warning for both?

Copy link
Member

Choose a reason for hiding this comment

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

It makes sense to warn the user that we don't support the value they asked for. Maybe in another PR if you like.

lib/jekyll/converters/scss.rb Show resolved Hide resolved
def file_path
return nil if associate_page_failed?

File.join(site_source_relative_from_pwd, sass_page.path)
Copy link
Member

Choose a reason for hiding this comment

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

This might need to use Jekyll.sanitized_path, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be safe as:

  • File.join is simply: base + '/' + path. It is concatenated as is without any expansion.
  • sass_page.path is the path to sass page e.g. css/main.scss - this is provided by jekyll. Since jekyll only process files inside current project, this should be safe to use.

Copy link
Member

Choose a reason for hiding this comment

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

What happens for symlinks?

Copy link
Contributor Author

@ntkme ntkme Oct 15, 2021

Choose a reason for hiding this comment

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

@parkr I just tested and Jekyll.sanitized_path does not do anything different for symlinks. If file is a symlink, sass_page.path still returns the original unresolved path.

In the end when Jekyll process "pages" collection, it does not seem to care if a "page" is a symlink to a file outside of the project or not. I would say that is a problem of Jekyll pages collection, not something we should deal with here.

@ashmaroli
Copy link
Member

Thank you @ntkme, for all the work put into this.
@jekyllbot: merge +minor

@jekyllbot jekyllbot merged commit f1b9817 into jekyll:master Oct 15, 2021
jekyllbot added a commit that referenced this pull request Oct 15, 2021
@ntkme
Copy link
Contributor Author

ntkme commented Oct 15, 2021

Thanks @ashmaroli @mattr- @parkr for your help!

12beesinatrenchcoat added a commit to 12beesinatrenchcoat/12beesinatrenchcoat.github.io that referenced this pull request Oct 17, 2021
@jekyll jekyll locked and limited conversation to collaborators Oct 15, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
6 participants